933d6fa15e5f3faa0054356e3bced059454066c4
[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('--hypervisor', dest='hypervisor',
387                         action='store',
388                         metavar='<hypervisor name>',
389                         help='Where chains must run ("compute", "az:", "az:compute")')
390
391     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
392                         action='store',
393                         metavar='<vlan>',
394                         help='Port to port or port to switch to port L2 loopback with VLAN id')
395
396     opts, unknown_opts = parser.parse_known_args()
397     return opts, unknown_opts
398
399
400 def load_default_config():
401     default_cfg = resource_string(__name__, "cfg.default.yaml")
402     config = config_loads(default_cfg)
403     config.name = '(built-in default config)'
404     return config, default_cfg
405
406
407 def override_custom_traffic(config, frame_sizes, unidir):
408     """Override the traffic profiles with a custom one."""
409     if frame_sizes is not None:
410         traffic_profile_name = "custom_traffic_profile"
411         config.traffic_profile = [
412             {
413                 "l2frame_size": frame_sizes,
414                 "name": traffic_profile_name
415             }
416         ]
417     else:
418         traffic_profile_name = config.traffic["profile"]
419
420     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
421     config.traffic = {
422         "bidirectional": bidirectional,
423         "profile": traffic_profile_name
424     }
425
426
427 def check_physnet(name, netattrs):
428     if not netattrs.physical_network:
429         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
430                         .format(n=name))
431     if not netattrs.segmentation_id:
432         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
433                         .format(n=name))
434
435 def status_cleanup(config, cleanup, force_cleanup):
436     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
437     # check if another run is pending
438     ret_code = 0
439     try:
440         with utils.RunLock():
441             LOG.info('Status: idle')
442     except Exception:
443         LOG.info('Status: busy (run pending)')
444         ret_code = 1
445     # check nfvbench resources
446     if config.openrc_file and config.service_chain != ChainType.EXT:
447         cleaner = Cleaner(config)
448         count = cleaner.show_resources()
449         if count and (cleanup or force_cleanup):
450             cleaner.clean(not force_cleanup)
451     sys.exit(ret_code)
452
453 def main():
454     global fluent_logger
455     run_summary_required = False
456     try:
457         log.setup()
458         # load default config file
459         config, default_cfg = load_default_config()
460         # create factory for platform specific classes
461         try:
462             factory_module = importlib.import_module(config['factory_module'])
463             factory = getattr(factory_module, config['factory_class'])()
464         except AttributeError:
465             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
466                             .format(m=config['factory_module'], c=config['factory_class']))
467         # create config plugin for this platform
468         config_plugin = factory.get_config_plugin_class()(config)
469         config = config_plugin.get_config()
470
471         opts, unknown_opts = parse_opts_from_cli()
472         log.set_level(debug=opts.debug)
473
474         if opts.version:
475             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
476             sys.exit(0)
477
478         if opts.summary:
479             with open(opts.summary) as json_data:
480                 result = json.load(json_data)
481                 if opts.user_label:
482                     result['config']['user_label'] = opts.user_label
483                 print NFVBenchSummarizer(result, fluent_logger)
484             sys.exit(0)
485
486         # show default config in text/yaml format
487         if opts.show_default_config:
488             print default_cfg
489             sys.exit(0)
490
491         config.name = ''
492         if opts.config:
493             # do not check extra_specs in flavor as it can contain any key/value pairs
494             whitelist_keys = ['extra_specs']
495             # override default config options with start config at path parsed from CLI
496             # check if it is an inline yaml/json config or a file name
497             if os.path.isfile(opts.config):
498                 LOG.info('Loading configuration file: %s', opts.config)
499                 config = config_load(opts.config, config, whitelist_keys)
500                 config.name = os.path.basename(opts.config)
501             else:
502                 LOG.info('Loading configuration string: %s', opts.config)
503                 config = config_loads(opts.config, config, whitelist_keys)
504
505         # setup the fluent logger as soon as possible right after the config plugin is called,
506         # if there is any logging or result tag is set then initialize the fluent logger
507         for fluentd in config.fluentd:
508             if fluentd.logging_tag or fluentd.result_tag:
509                 fluent_logger = FluentLogHandler(config.fluentd)
510                 LOG.addHandler(fluent_logger)
511                 break
512
513         # traffic profile override options
514         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
515
516         # copy over cli options that are used in config
517         config.generator_profile = opts.generator_profile
518         if opts.sriov:
519             config.sriov = True
520         if opts.log_file:
521             config.log_file = opts.log_file
522         if opts.service_chain:
523             config.service_chain = opts.service_chain
524         if opts.service_chain_count:
525             config.service_chain_count = opts.service_chain_count
526         if opts.no_vswitch_access:
527             config.no_vswitch_access = opts.no_vswitch_access
528         if opts.hypervisor:
529             # can be any of 'comp1', 'nova:', 'nova:comp1'
530             config.compute_nodes = opts.hypervisor
531
532         # port to port loopback (direct or through switch)
533         if opts.l2_loopback:
534             config.l2_loopback = True
535             if config.service_chain != ChainType.EXT:
536                 LOG.info('Changing service chain type to EXT')
537                 config.service_chain = ChainType.EXT
538             if not config.no_arp:
539                 LOG.info('Disabling ARP')
540                 config.no_arp = True
541             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
542             LOG.info('Running L2 loopback: using EXT chain/no ARP')
543
544         if opts.use_sriov_middle_net:
545             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
546                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
547             config.use_sriov_middle_net = True
548
549         if config.sriov and config.service_chain != ChainType.EXT:
550             # if sriov is requested (does not apply to ext chains)
551             # make sure the physnet names are specified
552             check_physnet("left", config.internal_networks.left)
553             check_physnet("right", config.internal_networks.right)
554             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
555                 check_physnet("middle", config.internal_networks.middle)
556
557         # show running config in json format
558         if opts.show_config:
559             print json.dumps(config, sort_keys=True, indent=4)
560             sys.exit(0)
561
562         # check that an empty openrc file (no OpenStack) is only allowed
563         # with EXT chain
564         if not config.openrc_file:
565             if config.service_chain == ChainType.EXT:
566                 LOG.info('EXT chain with OpenStack mode disabled')
567             else:
568                 raise Exception("openrc_file is empty in the configuration and is required")
569
570         # update the config in the config plugin as it might have changed
571         # in a copy of the dict (config plugin still holds the original dict)
572         config_plugin.set_config(config)
573
574         if opts.status or opts.cleanup or opts.force_cleanup:
575             status_cleanup(config, opts.cleanup, opts.force_cleanup)
576
577         # add file log if requested
578         if config.log_file:
579             log.add_file_logger(config.log_file)
580
581         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
582             else None
583
584         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
585
586         if opts.server:
587             if os.path.isdir(opts.server):
588                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
589                 nfvbench_instance.set_notifier(server)
590                 try:
591                     port = int(opts.port)
592                 except ValueError:
593                     server.run(host=opts.host)
594                 else:
595                     server.run(host=opts.host, port=port)
596             else:
597                 print 'Invalid HTTP root directory: ' + opts.server
598                 sys.exit(1)
599         else:
600             with utils.RunLock():
601                 run_summary_required = True
602                 if unknown_opts:
603                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
604                     LOG.error(err_msg)
605                     raise Exception(err_msg)
606
607                 # remove unfilled values
608                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
609                 # get CLI args
610                 params = ' '.join(str(e) for e in sys.argv[1:])
611                 result = nfvbench_instance.run(opts, params)
612                 if 'error_message' in result:
613                     raise Exception(result['error_message'])
614
615                 if 'result' in result and result['status']:
616                     nfvbench_instance.save(result['result'])
617                     nfvbench_instance.prepare_summary(result['result'])
618     except Exception as exc:
619         run_summary_required = True
620         LOG.error({
621             'status': NFVBench.STATUS_ERROR,
622             'error_message': traceback.format_exc()
623         })
624         print str(exc)
625     finally:
626         if fluent_logger:
627             # only send a summary record if there was an actual nfvbench run or
628             # if an error/exception was logged.
629             fluent_logger.send_run_summary(run_summary_required)
630
631
632 if __name__ == '__main__':
633     main()