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