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