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