2.0 beta NFVBENCH-91 Allow multi-chaining with separate edge networks
[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 import copy
19 import datetime
20 import importlib
21 import json
22 import os
23 import sys
24 import traceback
25
26 from attrdict import AttrDict
27 import pbr.version
28 from pkg_resources import resource_string
29
30 from __init__ import __version__
31 from chain_runner import ChainRunner
32 from cleanup import Cleaner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from fluentd import FluentLogHandler
37 import log
38 from log import LOG
39 from nfvbenchd import WebSocketIoServer
40 from specs import ChainType
41 from specs import Specs
42 from summarizer import NFVBenchSummarizer
43 import utils
44
45 fluent_logger = None
46
47
48 class NFVBench(object):
49     """Main class of NFV benchmarking tool."""
50
51     STATUS_OK = 'OK'
52     STATUS_ERROR = 'ERROR'
53
54     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
55         # the base config never changes for a given NFVbench instance
56         self.base_config = config
57         # this is the running config, updated at every run()
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             if config.openrc_file else None
64         self.chain_runner = None
65         self.specs = Specs()
66         self.specs.set_openstack_spec(openstack_spec)
67         self.vni_ports = []
68         sys.stdout.flush()
69
70     def set_notifier(self, notifier):
71         self.notifier = notifier
72
73     def run(self, opts, args):
74         status = NFVBench.STATUS_OK
75         result = None
76         message = ''
77         if fluent_logger:
78             # take a snapshot of the current time for this new run
79             # so that all subsequent logs can relate to this run
80             fluent_logger.start_new_run()
81         LOG.info(args)
82         try:
83             # recalc the running config based on the base config and options for this run
84             self._update_config(opts)
85             self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
86                                                                     self.specs.openstack))
87             self.chain_runner = ChainRunner(self.config,
88                                             self.cred,
89                                             self.specs,
90                                             self.factory,
91                                             self.notifier)
92             new_frame_sizes = []
93             min_packet_size = "68" if self.config.vlan_tagging else "64"
94             for frame_size in self.config.frame_sizes:
95                 try:
96                     if int(frame_size) < int(min_packet_size):
97                         new_frame_sizes.append(min_packet_size)
98                         LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to " +
99                                  "traffic generator restriction", frame_size, min_packet_size)
100                     else:
101                         new_frame_sizes.append(frame_size)
102                 except ValueError:
103                     new_frame_sizes.append(frame_size)
104             self.config.actual_frame_sizes = tuple(new_frame_sizes)
105             result = {
106                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
107                 "nfvbench_version": __version__,
108                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
109                 "benchmarks": {
110                     "network": {
111                         "service_chain": self.chain_runner.run(),
112                         "versions": self.chain_runner.get_version(),
113                     }
114                 }
115             }
116             if self.specs.openstack:
117                 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
118                                             "encaps": self.specs.openstack.encaps}
119             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
120         except Exception:
121             status = NFVBench.STATUS_ERROR
122             message = traceback.format_exc()
123         except KeyboardInterrupt:
124             status = NFVBench.STATUS_ERROR
125             message = traceback.format_exc()
126         finally:
127             if self.chain_runner:
128                 self.chain_runner.close()
129
130         if status == NFVBench.STATUS_OK:
131             # result2 = utils.dict_to_json_dict(result)
132             return {
133                 'status': status,
134                 'result': result
135             }
136         return {
137             'status': status,
138             'error_message': message
139         }
140
141     def prepare_summary(self, result):
142         """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
143         global fluent_logger
144         summary = NFVBenchSummarizer(result, fluent_logger)
145         LOG.info(str(summary))
146
147     def save(self, result):
148         """Save results in json format file."""
149         utils.save_json_result(result,
150                                self.config.json_file,
151                                self.config.std_json_path,
152                                self.config.service_chain,
153                                self.config.service_chain_count,
154                                self.config.flow_count,
155                                self.config.frame_sizes)
156
157     def _update_config(self, opts):
158         """Recalculate the running config based on the base config and opts.
159
160         Sanity check on the config is done here as well.
161         """
162         self.config = AttrDict(dict(self.base_config))
163         self.config.update(opts)
164         config = self.config
165
166         config.service_chain = config.service_chain.upper()
167         config.service_chain_count = int(config.service_chain_count)
168         if config.l2_loopback:
169             # force the number of chains to be 1 in case of l2 loopback
170             config.service_chain_count = 1
171             config.service_chain = ChainType.EXT
172             config.no_arp = True
173             LOG.info('Running L2 loopback: using EXT chain/no ARP')
174         config.flow_count = utils.parse_flow_count(config.flow_count)
175         required_flow_count = config.service_chain_count * 2
176         if config.flow_count < required_flow_count:
177             LOG.info("Flow count %d has been set to minimum value of '%d' "
178                      "for current configuration", config.flow_count,
179                      required_flow_count)
180             config.flow_count = required_flow_count
181
182         if config.flow_count % 2:
183             config.flow_count += 1
184
185         config.duration_sec = float(config.duration_sec)
186         config.interval_sec = float(config.interval_sec)
187         config.pause_sec = float(config.pause_sec)
188
189         if config.traffic is None or not config.traffic:
190             raise Exception("Missing traffic property in configuration")
191
192         if config.openrc_file:
193             config.openrc_file = os.path.expanduser(config.openrc_file)
194
195         config.ndr_run = (not config.no_traffic and
196                           'ndr' in config.rate.strip().lower().split('_'))
197         config.pdr_run = (not config.no_traffic and
198                           'pdr' in config.rate.strip().lower().split('_'))
199         config.single_run = (not config.no_traffic and
200                              not (config.ndr_run or config.pdr_run))
201
202         config.json_file = config.json if config.json else None
203         if config.json_file:
204             (path, _filename) = os.path.split(config.json)
205             if not os.path.exists(path):
206                 raise Exception('Please provide existing path for storing results in JSON file. '
207                                 'Path used: {path}'.format(path=path))
208
209         config.std_json_path = config.std_json if config.std_json else None
210         if config.std_json_path:
211             if not os.path.exists(config.std_json):
212                 raise Exception('Please provide existing path for storing results in JSON file. '
213                                 'Path used: {path}'.format(path=config.std_json_path))
214
215         self.config_plugin.validate_config(config, self.specs.openstack)
216
217
218 def parse_opts_from_cli():
219     parser = argparse.ArgumentParser()
220
221     parser.add_argument('--status', dest='status',
222                         action='store_true',
223                         default=None,
224                         help='Provide NFVbench status')
225
226     parser.add_argument('-c', '--config', dest='config',
227                         action='store',
228                         help='Override default values with a config file or '
229                              'a yaml/json config string',
230                         metavar='<file_name_or_yaml>')
231
232     parser.add_argument('--server', dest='server',
233                         default=None,
234                         action='store',
235                         metavar='<http_root_pathname>',
236                         help='Run nfvbench in server mode and pass'
237                              ' the HTTP root folder full pathname')
238
239     parser.add_argument('--host', dest='host',
240                         action='store',
241                         default='0.0.0.0',
242                         help='Host IP address on which server will be listening (default 0.0.0.0)')
243
244     parser.add_argument('-p', '--port', dest='port',
245                         action='store',
246                         default=7555,
247                         help='Port on which server will be listening (default 7555)')
248
249     parser.add_argument('-sc', '--service-chain', dest='service_chain',
250                         choices=ChainType.names,
251                         action='store',
252                         help='Service chain to run')
253
254     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
255                         action='store',
256                         help='Set number of service chains to run',
257                         metavar='<service_chain_count>')
258
259     parser.add_argument('-fc', '--flow-count', dest='flow_count',
260                         action='store',
261                         help='Set number of total flows for all chains and all directions',
262                         metavar='<flow_count>')
263
264     parser.add_argument('--rate', dest='rate',
265                         action='store',
266                         help='Specify rate in pps, bps or %% as total for all directions',
267                         metavar='<rate>')
268
269     parser.add_argument('--duration', dest='duration_sec',
270                         action='store',
271                         help='Set duration to run traffic generator (in seconds)',
272                         metavar='<duration_sec>')
273
274     parser.add_argument('--interval', dest='interval_sec',
275                         action='store',
276                         help='Set interval to record traffic generator stats (in seconds)',
277                         metavar='<interval_sec>')
278
279     parser.add_argument('--inter-node', dest='inter_node',
280                         default=None,
281                         action='store_true',
282                         help='run VMs in different compute nodes (PVVP only)')
283
284     parser.add_argument('--sriov', dest='sriov',
285                         default=None,
286                         action='store_true',
287                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
288
289     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
290                         default=None,
291                         action='store_true',
292                         help='Use SRIOV to handle the middle network traffic '
293                              '(PVVP with SRIOV only)')
294
295     parser.add_argument('-d', '--debug', dest='debug',
296                         action='store_true',
297                         default=None,
298                         help='print debug messages (verbose)')
299
300     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
301                         action='store',
302                         help='Traffic generator profile to use')
303
304     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
305                         default=None,
306                         action='store_true',
307                         help='Check config and connectivity only - do not generate traffic')
308
309     parser.add_argument('--no-arp', dest='no_arp',
310                         default=None,
311                         action='store_true',
312                         help='Do not use ARP to find MAC addresses, '
313                              'instead use values in config file')
314
315     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
316                         default=None,
317                         action='store_true',
318                         help='Skip vswitch configuration and retrieving of stats')
319
320     parser.add_argument('--no-cleanup', dest='no_cleanup',
321                         default=None,
322                         action='store_true',
323                         help='no cleanup after run')
324
325     parser.add_argument('--cleanup', dest='cleanup',
326                         default=None,
327                         action='store_true',
328                         help='Cleanup NFVbench resources (prompt to confirm)')
329
330     parser.add_argument('--force-cleanup', dest='force_cleanup',
331                         default=None,
332                         action='store_true',
333                         help='Cleanup NFVbench resources (do not prompt)')
334
335     parser.add_argument('--json', dest='json',
336                         action='store',
337                         help='store results in json format file',
338                         metavar='<path>/<filename>')
339
340     parser.add_argument('--std-json', dest='std_json',
341                         action='store',
342                         help='store results in json format file with nfvbench standard filename: '
343                              '<service-chain-type>-<service-chain-count>-<flow-count>'
344                              '-<packet-sizes>.json',
345                         metavar='<path>')
346
347     parser.add_argument('--show-default-config', dest='show_default_config',
348                         default=None,
349                         action='store_true',
350                         help='print the default config in yaml format (unedited)')
351
352     parser.add_argument('--show-config', dest='show_config',
353                         default=None,
354                         action='store_true',
355                         help='print the running config in json format')
356
357     parser.add_argument('-ss', '--show-summary', dest='summary',
358                         action='store',
359                         help='Show summary from nfvbench json file',
360                         metavar='<json>')
361
362     parser.add_argument('-v', '--version', dest='version',
363                         default=None,
364                         action='store_true',
365                         help='Show version')
366
367     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
368                         action='append',
369                         help='Override traffic profile frame sizes',
370                         metavar='<frame_size_bytes or IMIX>')
371
372     parser.add_argument('--unidir', dest='unidir',
373                         action='store_true',
374                         default=None,
375                         help='Override traffic profile direction (requires -fs)')
376
377     parser.add_argument('--log-file', '--logfile', dest='log_file',
378                         action='store',
379                         help='Filename for saving logs',
380                         metavar='<log_file>')
381
382     parser.add_argument('--user-label', '--userlabel', dest='user_label',
383                         action='store',
384                         help='Custom label for performance records')
385
386     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
387                         action='store',
388                         metavar='<vlan>',
389                         help='Port to port or port to switch to port L2 loopback with VLAN id')
390
391     opts, unknown_opts = parser.parse_known_args()
392     return opts, unknown_opts
393
394
395 def load_default_config():
396     default_cfg = resource_string(__name__, "cfg.default.yaml")
397     config = config_loads(default_cfg)
398     config.name = '(built-in default config)'
399     return config, default_cfg
400
401
402 def override_custom_traffic(config, frame_sizes, unidir):
403     """Override the traffic profiles with a custom one."""
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 def status_cleanup(config, cleanup, force_cleanup):
431     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
432     # check if another run is pending
433     ret_code = 0
434     try:
435         with utils.RunLock():
436             LOG.info('Status: idle')
437     except Exception:
438         LOG.info('Status: busy (run pending)')
439         ret_code = 1
440     # check nfvbench resources
441     if config.openrc_file and config.service_chain != ChainType.EXT:
442         cleaner = Cleaner(config)
443         count = cleaner.show_resources()
444         if count and (cleanup or force_cleanup):
445             cleaner.clean(not force_cleanup)
446     sys.exit(ret_code)
447
448 def main():
449     global fluent_logger
450     run_summary_required = False
451     try:
452         log.setup()
453         # load default config file
454         config, default_cfg = load_default_config()
455         # create factory for platform specific classes
456         try:
457             factory_module = importlib.import_module(config['factory_module'])
458             factory = getattr(factory_module, config['factory_class'])()
459         except AttributeError:
460             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
461                             .format(m=config['factory_module'], c=config['factory_class']))
462         # create config plugin for this platform
463         config_plugin = factory.get_config_plugin_class()(config)
464         config = config_plugin.get_config()
465
466         opts, unknown_opts = parse_opts_from_cli()
467         log.set_level(debug=opts.debug)
468
469         if opts.version:
470             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
471             sys.exit(0)
472
473         if opts.summary:
474             with open(opts.summary) as json_data:
475                 result = json.load(json_data)
476                 if opts.user_label:
477                     result['config']['user_label'] = opts.user_label
478                 print NFVBenchSummarizer(result, fluent_logger)
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: %s', 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: %s', opts.config)
498                 config = config_loads(opts.config, config, whitelist_keys)
499
500         # setup the fluent logger as soon as possible right after the config plugin is called,
501         # if there is any logging or result tag is set then initialize the fluent logger
502         for fluentd in config.fluentd:
503             if fluentd.logging_tag or fluentd.result_tag:
504                 fluent_logger = FluentLogHandler(config.fluentd)
505                 LOG.addHandler(fluent_logger)
506                 break
507
508         # traffic profile override options
509         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
510
511         # copy over cli options that are used in config
512         config.generator_profile = opts.generator_profile
513         if opts.sriov:
514             config.sriov = True
515         if opts.log_file:
516             config.log_file = opts.log_file
517         if opts.service_chain:
518             config.service_chain = opts.service_chain
519         if opts.service_chain_count:
520             config.service_chain_count = opts.service_chain_count
521         if opts.no_vswitch_access:
522             config.no_vswitch_access = opts.no_vswitch_access
523
524         # port to port loopback (direct or through switch)
525         if opts.l2_loopback:
526             config.l2_loopback = True
527             if config.service_chain != ChainType.EXT:
528                 LOG.info('Changing service chain type to EXT')
529                 config.service_chain = ChainType.EXT
530             if not config.no_arp:
531                 LOG.info('Disabling ARP')
532                 config.no_arp = True
533             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
534             LOG.info('Running L2 loopback: using EXT chain/no ARP')
535
536         if opts.use_sriov_middle_net:
537             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
538                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
539             config.use_sriov_middle_net = True
540
541         if config.sriov and config.service_chain != ChainType.EXT:
542             # if sriov is requested (does not apply to ext chains)
543             # make sure the physnet names are specified
544             check_physnet("left", config.internal_networks.left)
545             check_physnet("right", config.internal_networks.right)
546             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
547                 check_physnet("middle", config.internal_networks.middle)
548
549         # show running config in json format
550         if opts.show_config:
551             print json.dumps(config, sort_keys=True, indent=4)
552             sys.exit(0)
553
554         # check that an empty openrc file (no OpenStack) is only allowed
555         # with EXT chain
556         if not config.openrc_file:
557             if config.service_chain == ChainType.EXT:
558                 LOG.info('EXT chain with OpenStack mode disabled')
559             else:
560                 raise Exception("openrc_file is empty in the configuration and is required")
561
562         # update the config in the config plugin as it might have changed
563         # in a copy of the dict (config plugin still holds the original dict)
564         config_plugin.set_config(config)
565
566         if opts.status or opts.cleanup or opts.force_cleanup:
567             status_cleanup(config, opts.cleanup, opts.force_cleanup)
568
569         # add file log if requested
570         if config.log_file:
571             log.add_file_logger(config.log_file)
572
573         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
574             else None
575
576         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
577
578         if opts.server:
579             if os.path.isdir(opts.server):
580                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
581                 nfvbench_instance.set_notifier(server)
582                 try:
583                     port = int(opts.port)
584                 except ValueError:
585                     server.run(host=opts.host)
586                 else:
587                     server.run(host=opts.host, port=port)
588             else:
589                 print 'Invalid HTTP root directory: ' + opts.server
590                 sys.exit(1)
591         else:
592             with utils.RunLock():
593                 run_summary_required = True
594                 if unknown_opts:
595                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
596                     LOG.error(err_msg)
597                     raise Exception(err_msg)
598
599                 # remove unfilled values
600                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
601                 # get CLI args
602                 params = ' '.join(str(e) for e in sys.argv[1:])
603                 result = nfvbench_instance.run(opts, params)
604                 if 'error_message' in result:
605                     raise Exception(result['error_message'])
606
607                 if 'result' in result and result['status']:
608                     nfvbench_instance.save(result['result'])
609                     nfvbench_instance.prepare_summary(result['result'])
610     except Exception as exc:
611         run_summary_required = True
612         LOG.error({
613             'status': NFVBench.STATUS_ERROR,
614             'error_message': traceback.format_exc()
615         })
616         print str(exc)
617     finally:
618         if fluent_logger:
619             # only send a summary record if there was an actual nfvbench run or
620             # if an error/exception was logged.
621             fluent_logger.send_run_summary(run_summary_required)
622
623
624 if __name__ == '__main__':
625     main()