5e2de769279803c69b36c400c5f0811887678223
[nfvbench.git] / nfvbench / nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16
17 import argparse
18 from collections import defaultdict
19 import copy
20 import datetime
21 import importlib
22 import json
23 import os
24 import sys
25 import traceback
26
27 from attrdict import AttrDict
28 import pbr.version
29 from pkg_resources import resource_string
30
31 from __init__ import __version__
32 from chain_runner import ChainRunner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from factory import BasicFactory
37 from fluentd import FluentLogHandler
38 import log
39 from log import LOG
40 from nfvbenchd import WebSocketIoServer
41 from specs import ChainType
42 from specs import Specs
43 from summarizer import NFVBenchSummarizer
44 from traffic_client import TrafficGeneratorFactory
45 import utils
46
47 fluent_logger = None
48
49
50 class NFVBench(object):
51     """Main class of NFV benchmarking tool."""
52     STATUS_OK = 'OK'
53     STATUS_ERROR = 'ERROR'
54
55     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56         self.base_config = config
57         self.config = None
58         self.config_plugin = config_plugin
59         self.factory = factory
60         self.notifier = notifier
61         self.cred = credentials.Credentials(config.openrc_file, None, False) \
62             if config.openrc_file else None
63         self.chain_runner = None
64         self.specs = Specs()
65         self.specs.set_openstack_spec(openstack_spec)
66         self.clients = defaultdict(lambda: None)
67         self.vni_ports = []
68         sys.stdout.flush()
69
70     def setup(self):
71         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config, self.specs.openstack))
72         self.chain_runner = ChainRunner(self.config,
73                                         self.clients,
74                                         self.cred,
75                                         self.specs,
76                                         self.factory,
77                                         self.notifier)
78
79     def set_notifier(self, notifier):
80         self.notifier = notifier
81
82     def run(self, opts, args):
83         status = NFVBench.STATUS_OK
84         result = None
85         message = ''
86         if fluent_logger:
87             # take a snapshot of the current time for this new run
88             # so that all subsequent logs can relate to this run
89             fluent_logger.start_new_run()
90         LOG.info(args)
91         try:
92             self.update_config(opts)
93             self.setup()
94             new_frame_sizes = []
95             min_packet_size = "68" if self.config.vlan_tagging else "64"
96             for frame_size in self.config.frame_sizes:
97                 try:
98                     if int(frame_size) < int(min_packet_size):
99                         new_frame_sizes.append(min_packet_size)
100                         LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to "
101                                  + "traffic generator restriction", frame_size, min_packet_size)
102                     else:
103                         new_frame_sizes.append(frame_size)
104                 except ValueError:
105                     new_frame_sizes.append(frame_size)
106             self.config.frame_sizes = tuple(new_frame_sizes)
107             result = {
108                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
109                 "nfvbench_version": __version__,
110                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
111                 "benchmarks": {
112                     "network": {
113                         "service_chain": self.chain_runner.run(),
114                         "versions": self.chain_runner.get_version(),
115                     }
116                 }
117             }
118             if self.specs.openstack:
119                 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
120                                             "encaps": self.specs.openstack.encaps}
121             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
122         except Exception:
123             status = NFVBench.STATUS_ERROR
124             message = traceback.format_exc()
125         except KeyboardInterrupt:
126             status = NFVBench.STATUS_ERROR
127             message = traceback.format_exc()
128         finally:
129             if self.chain_runner:
130                 self.chain_runner.close()
131
132         if status == NFVBench.STATUS_OK:
133             result = utils.dict_to_json_dict(result)
134             return {
135                 'status': status,
136                 'result': result
137             }
138         return {
139             'status': status,
140             'error_message': message
141         }
142
143     def prepare_summary(self, result):
144         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
145         global fluent_logger
146         summary = NFVBenchSummarizer(result, fluent_logger)
147         LOG.info(str(summary))
148
149     def save(self, result):
150         """Save results in json format file."""
151         utils.save_json_result(result,
152                                self.config.json_file,
153                                self.config.std_json_path,
154                                self.config.service_chain,
155                                self.config.service_chain_count,
156                                self.config.flow_count,
157                                self.config.frame_sizes)
158
159     def update_config(self, opts):
160         self.config = AttrDict(dict(self.base_config))
161         self.config.update(opts)
162
163         self.config.service_chain = self.config.service_chain.upper()
164         self.config.service_chain_count = int(self.config.service_chain_count)
165         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
166         required_flow_count = self.config.service_chain_count * 2
167         if self.config.flow_count < required_flow_count:
168             LOG.info("Flow count %d has been set to minimum value of '%d' "
169                      "for current configuration", self.config.flow_count,
170                      required_flow_count)
171             self.config.flow_count = required_flow_count
172
173         if self.config.flow_count % 2 != 0:
174             self.config.flow_count += 1
175
176         self.config.duration_sec = float(self.config.duration_sec)
177         self.config.interval_sec = float(self.config.interval_sec)
178
179         # Get traffic generator profile config
180         if not self.config.generator_profile:
181             self.config.generator_profile = self.config.traffic_generator.default_profile
182
183         generator_factory = TrafficGeneratorFactory(self.config)
184         self.config.generator_config = \
185             generator_factory.get_generator_config(self.config.generator_profile)
186
187         # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
188         if self.config.service_chain == ChainType.EXT and self.config.no_arp:
189             if not (self.config.generator_config.mac_addrs_left is None and
190                     self.config.generator_config.mac_addrs_right is None):
191                 if (self.config.generator_config.mac_addrs_left is None or
192                         self.config.generator_config.mac_addrs_right is None):
193                     raise Exception("mac_addrs_left and mac_addrs_right must either "
194                                     "both be None or have a number of entries matching "
195                                     "service_chain_count")
196                 if not (len(self.config.generator_config.mac_addrs_left) ==
197                         self.config.service_chain_count and
198                         len(self.config.generator_config.mac_addrs_right) ==
199                         self.config.service_chain_count):
200                     raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
201                                     "does not match service_chain_count ({c})"
202                                     .format(a=len(self.config.generator_config.mac_addrs_left),
203                                             b=len(self.config.generator_config.mac_addrs_right),
204                                             c=self.config.service_chain_count))
205
206         if not any(self.config.generator_config.pcis):
207             raise Exception("PCI addresses configuration for selected traffic generator profile "
208                             "({tg_profile}) are missing. Please specify them in configuration file."
209                             .format(tg_profile=self.config.generator_profile))
210
211         if self.config.traffic is None or not self.config.traffic:
212             raise Exception("No traffic profile found in traffic configuration, "
213                             "please fill 'traffic' section in configuration file.")
214
215         if isinstance(self.config.traffic, tuple):
216             self.config.traffic = self.config.traffic[0]
217
218         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
219
220         self.config.ipv6_mode = False
221         self.config.no_dhcp = True
222         self.config.same_network_only = True
223         if self.config.openrc_file:
224             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
225
226         self.config.ndr_run = (not self.config.no_traffic
227                                and 'ndr' in self.config.rate.strip().lower().split('_'))
228         self.config.pdr_run = (not self.config.no_traffic
229                                and 'pdr' in self.config.rate.strip().lower().split('_'))
230         self.config.single_run = (not self.config.no_traffic
231                                   and not (self.config.ndr_run or self.config.pdr_run))
232
233         if self.config.vlans and len(self.config.vlans) != 2:
234             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
235
236         self.config.json_file = self.config.json if self.config.json else None
237         if self.config.json_file:
238             (path, _filename) = os.path.split(self.config.json)
239             if not os.path.exists(path):
240                 raise Exception('Please provide existing path for storing results in JSON file. '
241                                 'Path used: {path}'.format(path=path))
242
243         self.config.std_json_path = self.config.std_json if self.config.std_json else None
244         if self.config.std_json_path:
245             if not os.path.exists(self.config.std_json):
246                 raise Exception('Please provide existing path for storing results in JSON file. '
247                                 'Path used: {path}'.format(path=self.config.std_json_path))
248
249         self.config_plugin.validate_config(self.config, self.specs.openstack)
250
251
252 def parse_opts_from_cli():
253     parser = argparse.ArgumentParser()
254
255     parser.add_argument('-c', '--config', dest='config',
256                         action='store',
257                         help='Override default values with a config file or '
258                              'a yaml/json config string',
259                         metavar='<file_name_or_yaml>')
260
261     parser.add_argument('--server', dest='server',
262                         default=None,
263                         action='store',
264                         metavar='<http_root_pathname>',
265                         help='Run nfvbench in server mode and pass'
266                              ' the HTTP root folder full pathname')
267
268     parser.add_argument('--host', dest='host',
269                         action='store',
270                         default='0.0.0.0',
271                         help='Host IP address on which server will be listening (default 0.0.0.0)')
272
273     parser.add_argument('-p', '--port', dest='port',
274                         action='store',
275                         default=7555,
276                         help='Port on which server will be listening (default 7555)')
277
278     parser.add_argument('-sc', '--service-chain', dest='service_chain',
279                         choices=BasicFactory.chain_classes,
280                         action='store',
281                         help='Service chain to run')
282
283     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
284                         action='store',
285                         help='Set number of service chains to run',
286                         metavar='<service_chain_count>')
287
288     parser.add_argument('-fc', '--flow-count', dest='flow_count',
289                         action='store',
290                         help='Set number of total flows for all chains and all directions',
291                         metavar='<flow_count>')
292
293     parser.add_argument('--rate', dest='rate',
294                         action='store',
295                         help='Specify rate in pps, bps or %% as total for all directions',
296                         metavar='<rate>')
297
298     parser.add_argument('--duration', dest='duration_sec',
299                         action='store',
300                         help='Set duration to run traffic generator (in seconds)',
301                         metavar='<duration_sec>')
302
303     parser.add_argument('--interval', dest='interval_sec',
304                         action='store',
305                         help='Set interval to record traffic generator stats (in seconds)',
306                         metavar='<interval_sec>')
307
308     parser.add_argument('--inter-node', dest='inter_node',
309                         default=None,
310                         action='store_true',
311                         help='run VMs in different compute nodes (PVVP only)')
312
313     parser.add_argument('--sriov', dest='sriov',
314                         default=None,
315                         action='store_true',
316                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
317
318     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
319                         default=None,
320                         action='store_true',
321                         help='Use SRIOV to handle the middle network traffic '
322                              '(PVVP with SRIOV only)')
323
324     parser.add_argument('-d', '--debug', dest='debug',
325                         action='store_true',
326                         default=None,
327                         help='print debug messages (verbose)')
328
329     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
330                         action='store',
331                         help='Traffic generator profile to use')
332
333     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
334                         default=None,
335                         action='store_true',
336                         help='Check config and connectivity only - do not generate traffic')
337
338     parser.add_argument('--no-arp', dest='no_arp',
339                         default=None,
340                         action='store_true',
341                         help='Do not use ARP to find MAC addresses, '
342                              'instead use values in config file')
343
344     parser.add_argument('--no-reset', dest='no_reset',
345                         default=None,
346                         action='store_true',
347                         help='Do not reset counters prior to running')
348
349     parser.add_argument('--no-int-config', dest='no_int_config',
350                         default=None,
351                         action='store_true',
352                         help='Skip interfaces config on EXT service chain')
353
354     parser.add_argument('--no-tor-access', dest='no_tor_access',
355                         default=None,
356                         action='store_true',
357                         help='Skip TOR switch configuration and retrieving of stats')
358
359     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
360                         default=None,
361                         action='store_true',
362                         help='Skip vswitch configuration and retrieving of stats')
363
364     parser.add_argument('--no-cleanup', dest='no_cleanup',
365                         default=None,
366                         action='store_true',
367                         help='no cleanup after run')
368
369     parser.add_argument('--json', dest='json',
370                         action='store',
371                         help='store results in json format file',
372                         metavar='<path>/<filename>')
373
374     parser.add_argument('--std-json', dest='std_json',
375                         action='store',
376                         help='store results in json format file with nfvbench standard filename: '
377                              '<service-chain-type>-<service-chain-count>-<flow-count>'
378                              '-<packet-sizes>.json',
379                         metavar='<path>')
380
381     parser.add_argument('--show-default-config', dest='show_default_config',
382                         default=None,
383                         action='store_true',
384                         help='print the default config in yaml format (unedited)')
385
386     parser.add_argument('--show-config', dest='show_config',
387                         default=None,
388                         action='store_true',
389                         help='print the running config in json format')
390
391     parser.add_argument('-ss', '--show-summary', dest='summary',
392                         action='store',
393                         help='Show summary from nfvbench json file',
394                         metavar='<json>')
395
396     parser.add_argument('-v', '--version', dest='version',
397                         default=None,
398                         action='store_true',
399                         help='Show version')
400
401     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
402                         action='append',
403                         help='Override traffic profile frame sizes',
404                         metavar='<frame_size_bytes or IMIX>')
405
406     parser.add_argument('--unidir', dest='unidir',
407                         action='store_true',
408                         default=None,
409                         help='Override traffic profile direction (requires -fs)')
410
411     parser.add_argument('--log-file', '--logfile', dest='log_file',
412                         action='store',
413                         help='Filename for saving logs',
414                         metavar='<log_file>')
415
416     parser.add_argument('--user-label', '--userlabel', dest='user_label',
417                         action='store',
418                         help='Custom label for performance records')
419
420     opts, unknown_opts = parser.parse_known_args()
421     return opts, unknown_opts
422
423
424 def load_default_config():
425     default_cfg = resource_string(__name__, "cfg.default.yaml")
426     config = config_loads(default_cfg)
427     config.name = '(built-in default config)'
428     return config, default_cfg
429
430
431 def override_custom_traffic(config, frame_sizes, unidir):
432     """Override the traffic profiles with a custom one
433     """
434     if frame_sizes is not None:
435         traffic_profile_name = "custom_traffic_profile"
436         config.traffic_profile = [
437             {
438                 "l2frame_size": frame_sizes,
439                 "name": traffic_profile_name
440             }
441         ]
442     else:
443         traffic_profile_name = config.traffic["profile"]
444
445     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
446     config.traffic = {
447         "bidirectional": bidirectional,
448         "profile": traffic_profile_name
449     }
450
451
452 def check_physnet(name, netattrs):
453     if not netattrs.physical_network:
454         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
455                         .format(n=name))
456     if not netattrs.segmentation_id:
457         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
458                         .format(n=name))
459
460
461 def main():
462     global fluent_logger
463     run_summary_required = False
464     try:
465         log.setup()
466         # load default config file
467         config, default_cfg = load_default_config()
468         # create factory for platform specific classes
469         try:
470             factory_module = importlib.import_module(config['factory_module'])
471             factory = getattr(factory_module, config['factory_class'])()
472         except AttributeError:
473             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
474                             .format(m=config['factory_module'], c=config['factory_class']))
475         # create config plugin for this platform
476         config_plugin = factory.get_config_plugin_class()(config)
477         config = config_plugin.get_config()
478
479         opts, unknown_opts = parse_opts_from_cli()
480         log.set_level(debug=opts.debug)
481
482         if opts.version:
483             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
484             sys.exit(0)
485
486         if opts.summary:
487             with open(opts.summary) as json_data:
488                 result = json.load(json_data)
489                 if opts.user_label:
490                     result['config']['user_label'] = opts.user_label
491                 print NFVBenchSummarizer(result, fluent_logger)
492             sys.exit(0)
493
494         # show default config in text/yaml format
495         if opts.show_default_config:
496             print default_cfg
497             sys.exit(0)
498
499         config.name = ''
500         if opts.config:
501             # do not check extra_specs in flavor as it can contain any key/value pairs
502             whitelist_keys = ['extra_specs']
503             # override default config options with start config at path parsed from CLI
504             # check if it is an inline yaml/json config or a file name
505             if os.path.isfile(opts.config):
506                 LOG.info('Loading configuration file: %s', opts.config)
507                 config = config_load(opts.config, config, whitelist_keys)
508                 config.name = os.path.basename(opts.config)
509             else:
510                 LOG.info('Loading configuration string: %s', opts.config)
511                 config = config_loads(opts.config, config, whitelist_keys)
512
513         # setup the fluent logger as soon as possible right after the config plugin is called,
514         # if there is any logging or result tag is set then initialize the fluent logger
515         for fluentd in config.fluentd:
516             if fluentd.logging_tag or fluentd.result_tag:
517                 fluent_logger = FluentLogHandler(config.fluentd)
518                 LOG.addHandler(fluent_logger)
519                 break
520
521         # traffic profile override options
522         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
523
524         # copy over cli options that are used in config
525         config.generator_profile = opts.generator_profile
526         if opts.sriov:
527             config.sriov = True
528         if opts.log_file:
529             config.log_file = opts.log_file
530         if opts.service_chain:
531             config.service_chain = opts.service_chain
532         if opts.service_chain_count:
533             config.service_chain_count = opts.service_chain_count
534         if opts.no_vswitch_access:
535             config.no_vswitch_access = opts.no_vswitch_access
536         if opts.no_int_config:
537             config.no_int_config = opts.no_int_config
538
539         if opts.use_sriov_middle_net:
540             if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
541                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
542             config.use_sriov_middle_net = True
543
544         if config.sriov and config.service_chain != ChainType.EXT:
545             # if sriov is requested (does not apply to ext chains)
546             # make sure the physnet names are specified
547             check_physnet("left", config.internal_networks.left)
548             check_physnet("right", config.internal_networks.right)
549             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
550                 check_physnet("middle", config.internal_networks.middle)
551
552         # show running config in json format
553         if opts.show_config:
554             print json.dumps(config, sort_keys=True, indent=4)
555             sys.exit(0)
556
557         # check that an empty openrc file (no OpenStack) is only allowed
558         # with EXT chain
559         if not config.openrc_file:
560             if config.service_chain == ChainType.EXT:
561                 LOG.info('EXT chain with OpenStack mode disabled')
562             else:
563                 raise Exception("openrc_file is empty in the configuration and is required")
564
565         # update the config in the config plugin as it might have changed
566         # in a copy of the dict (config plugin still holds the original dict)
567         config_plugin.set_config(config)
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()