NFVBENCH-136 Add support for multiqueue for PVP/PVVP chains
[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             if config.flavor.vcpus < 2:
207                 raise Exception("Flavor vcpus must be >= 2")
208
209
210         config.ndr_run = (not config.no_traffic and
211                           'ndr' in config.rate.strip().lower().split('_'))
212         config.pdr_run = (not config.no_traffic and
213                           'pdr' in config.rate.strip().lower().split('_'))
214         config.single_run = (not config.no_traffic and
215                              not (config.ndr_run or config.pdr_run))
216
217         config.json_file = config.json if config.json else None
218         if config.json_file:
219             (path, _filename) = os.path.split(config.json)
220             if not os.path.exists(path):
221                 raise Exception('Please provide existing path for storing results in JSON file. '
222                                 'Path used: {path}'.format(path=path))
223
224         config.std_json_path = config.std_json if config.std_json else None
225         if config.std_json_path:
226             if not os.path.exists(config.std_json):
227                 raise Exception('Please provide existing path for storing results in JSON file. '
228                                 'Path used: {path}'.format(path=config.std_json_path))
229
230         # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
231         if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
232             raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
233                             config.vif_multiqueue_size)
234
235         # VxLAN sanity checks
236         if config.vxlan:
237             if config.vlan_tagging:
238                 config.vlan_tagging = False
239                 LOG.info('VxLAN: vlan_tagging forced to False '
240                          '(inner VLAN tagging must be disabled)')
241
242         self.config_plugin.validate_config(config, self.specs.openstack)
243
244
245 def _parse_opts_from_cli():
246     parser = argparse.ArgumentParser()
247
248     parser.add_argument('--status', dest='status',
249                         action='store_true',
250                         default=None,
251                         help='Provide NFVbench status')
252
253     parser.add_argument('-c', '--config', dest='config',
254                         action='store',
255                         help='Override default values with a config file or '
256                              'a yaml/json config string',
257                         metavar='<file_name_or_yaml>')
258
259     parser.add_argument('--server', dest='server',
260                         default=None,
261                         action='store_true',
262                         help='Run nfvbench in server mode')
263
264     parser.add_argument('--host', dest='host',
265                         action='store',
266                         default='0.0.0.0',
267                         help='Host IP address on which server will be listening (default 0.0.0.0)')
268
269     parser.add_argument('-p', '--port', dest='port',
270                         action='store',
271                         default=7555,
272                         help='Port on which server will be listening (default 7555)')
273
274     parser.add_argument('-sc', '--service-chain', dest='service_chain',
275                         choices=ChainType.names,
276                         action='store',
277                         help='Service chain to run')
278
279     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
280                         action='store',
281                         help='Set number of service chains to run',
282                         metavar='<service_chain_count>')
283
284     parser.add_argument('-fc', '--flow-count', dest='flow_count',
285                         action='store',
286                         help='Set number of total flows for all chains and all directions',
287                         metavar='<flow_count>')
288
289     parser.add_argument('--rate', dest='rate',
290                         action='store',
291                         help='Specify rate in pps, bps or %% as total for all directions',
292                         metavar='<rate>')
293
294     parser.add_argument('--duration', dest='duration_sec',
295                         action='store',
296                         help='Set duration to run traffic generator (in seconds)',
297                         metavar='<duration_sec>')
298
299     parser.add_argument('--interval', dest='interval_sec',
300                         action='store',
301                         help='Set interval to record traffic generator stats (in seconds)',
302                         metavar='<interval_sec>')
303
304     parser.add_argument('--inter-node', dest='inter_node',
305                         default=None,
306                         action='store_true',
307                         help='(deprecated)')
308
309     parser.add_argument('--sriov', dest='sriov',
310                         default=None,
311                         action='store_true',
312                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
313
314     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
315                         default=None,
316                         action='store_true',
317                         help='Use SRIOV to handle the middle network traffic '
318                              '(PVVP with SRIOV only)')
319
320     parser.add_argument('-d', '--debug', dest='debug',
321                         action='store_true',
322                         default=None,
323                         help='print debug messages (verbose)')
324
325     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
326                         action='store',
327                         help='Traffic generator profile to use')
328
329     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
330                         default=None,
331                         action='store_true',
332                         help='Check config and connectivity only - do not generate traffic')
333
334     parser.add_argument('--no-arp', dest='no_arp',
335                         default=None,
336                         action='store_true',
337                         help='Do not use ARP to find MAC addresses, '
338                              'instead use values in config file')
339
340     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
341                         default=None,
342                         action='store_true',
343                         help='Skip vswitch configuration and retrieving of stats')
344
345     parser.add_argument('--vxlan', dest='vxlan',
346                         default=None,
347                         action='store_true',
348                         help='Enable VxLan encapsulation')
349
350     parser.add_argument('--no-cleanup', dest='no_cleanup',
351                         default=None,
352                         action='store_true',
353                         help='no cleanup after run')
354
355     parser.add_argument('--cleanup', dest='cleanup',
356                         default=None,
357                         action='store_true',
358                         help='Cleanup NFVbench resources (prompt to confirm)')
359
360     parser.add_argument('--force-cleanup', dest='force_cleanup',
361                         default=None,
362                         action='store_true',
363                         help='Cleanup NFVbench resources (do not prompt)')
364
365     parser.add_argument('--restart', dest='restart',
366                         default=None,
367                         action='store_true',
368                         help='Restart TRex server')
369
370     parser.add_argument('--json', dest='json',
371                         action='store',
372                         help='store results in json format file',
373                         metavar='<path>/<filename>')
374
375     parser.add_argument('--std-json', dest='std_json',
376                         action='store',
377                         help='store results in json format file with nfvbench standard filename: '
378                              '<service-chain-type>-<service-chain-count>-<flow-count>'
379                              '-<packet-sizes>.json',
380                         metavar='<path>')
381
382     parser.add_argument('--show-default-config', dest='show_default_config',
383                         default=None,
384                         action='store_true',
385                         help='print the default config in yaml format (unedited)')
386
387     parser.add_argument('--show-config', dest='show_config',
388                         default=None,
389                         action='store_true',
390                         help='print the running config in json format')
391
392     parser.add_argument('-ss', '--show-summary', dest='summary',
393                         action='store',
394                         help='Show summary from nfvbench json file',
395                         metavar='<json>')
396
397     parser.add_argument('-v', '--version', dest='version',
398                         default=None,
399                         action='store_true',
400                         help='Show version')
401
402     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
403                         action='append',
404                         help='Override traffic profile frame sizes',
405                         metavar='<frame_size_bytes or IMIX>')
406
407     parser.add_argument('--unidir', dest='unidir',
408                         action='store_true',
409                         default=None,
410                         help='Override traffic profile direction (requires -fs)')
411
412     parser.add_argument('--log-file', '--logfile', dest='log_file',
413                         action='store',
414                         help='Filename for saving logs',
415                         metavar='<log_file>')
416
417     parser.add_argument('--user-label', '--userlabel', dest='user_label',
418                         action='store',
419                         help='Custom label for performance records')
420
421     parser.add_argument('--hypervisor', dest='hypervisor',
422                         action='store',
423                         metavar='<hypervisor name>',
424                         help='Where chains must run ("compute", "az:", "az:compute")')
425
426     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
427                         action='store',
428                         metavar='<vlan>',
429                         help='Port to port or port to switch to port L2 loopback with VLAN id')
430
431     opts, unknown_opts = parser.parse_known_args()
432     return opts, unknown_opts
433
434
435 def load_default_config():
436     default_cfg = resource_string(__name__, "cfg.default.yaml")
437     config = config_loads(default_cfg)
438     config.name = '(built-in default config)'
439     return config, default_cfg
440
441
442 def override_custom_traffic(config, frame_sizes, unidir):
443     """Override the traffic profiles with a custom one."""
444     if frame_sizes is not None:
445         traffic_profile_name = "custom_traffic_profile"
446         config.traffic_profile = [
447             {
448                 "l2frame_size": frame_sizes,
449                 "name": traffic_profile_name
450             }
451         ]
452     else:
453         traffic_profile_name = config.traffic["profile"]
454
455     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
456     config.traffic = {
457         "bidirectional": bidirectional,
458         "profile": traffic_profile_name
459     }
460
461
462 def check_physnet(name, netattrs):
463     if not netattrs.physical_network:
464         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
465                         .format(n=name))
466     if not netattrs.segmentation_id:
467         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
468                         .format(n=name))
469
470 def status_cleanup(config, cleanup, force_cleanup):
471     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
472     # check if another run is pending
473     ret_code = 0
474     try:
475         with utils.RunLock():
476             LOG.info('Status: idle')
477     except Exception:
478         LOG.info('Status: busy (run pending)')
479         ret_code = 1
480     # check nfvbench resources
481     if config.openrc_file and config.service_chain != ChainType.EXT:
482         cleaner = Cleaner(config)
483         count = cleaner.show_resources()
484         if count and (cleanup or force_cleanup):
485             cleaner.clean(not force_cleanup)
486     sys.exit(ret_code)
487
488 def main():
489     global fluent_logger
490     run_summary_required = False
491     try:
492         log.setup()
493         # load default config file
494         config, default_cfg = load_default_config()
495         # create factory for platform specific classes
496         try:
497             factory_module = importlib.import_module(config['factory_module'])
498             factory = getattr(factory_module, config['factory_class'])()
499         except AttributeError:
500             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
501                             .format(m=config['factory_module'], c=config['factory_class']))
502         # create config plugin for this platform
503         config_plugin = factory.get_config_plugin_class()(config)
504         config = config_plugin.get_config()
505
506         opts, unknown_opts = _parse_opts_from_cli()
507         log.set_level(debug=opts.debug)
508
509         if opts.version:
510             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
511             sys.exit(0)
512
513         if opts.summary:
514             with open(opts.summary) as json_data:
515                 result = json.load(json_data)
516                 if opts.user_label:
517                     result['config']['user_label'] = opts.user_label
518                 print NFVBenchSummarizer(result, fluent_logger)
519             sys.exit(0)
520
521         # show default config in text/yaml format
522         if opts.show_default_config:
523             print default_cfg
524             sys.exit(0)
525
526         config.name = ''
527         if opts.config:
528             # do not check extra_specs in flavor as it can contain any key/value pairs
529             whitelist_keys = ['extra_specs']
530             # override default config options with start config at path parsed from CLI
531             # check if it is an inline yaml/json config or a file name
532             if os.path.isfile(opts.config):
533                 LOG.info('Loading configuration file: %s', opts.config)
534                 config = config_load(opts.config, config, whitelist_keys)
535                 config.name = os.path.basename(opts.config)
536             else:
537                 LOG.info('Loading configuration string: %s', opts.config)
538                 config = config_loads(opts.config, config, whitelist_keys)
539
540         # setup the fluent logger as soon as possible right after the config plugin is called,
541         # if there is any logging or result tag is set then initialize the fluent logger
542         for fluentd in config.fluentd:
543             if fluentd.logging_tag or fluentd.result_tag:
544                 fluent_logger = FluentLogHandler(config.fluentd)
545                 LOG.addHandler(fluent_logger)
546                 break
547
548         # traffic profile override options
549         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
550
551         # copy over cli options that are used in config
552         config.generator_profile = opts.generator_profile
553         if opts.sriov:
554             config.sriov = True
555         if opts.log_file:
556             config.log_file = opts.log_file
557         if opts.service_chain:
558             config.service_chain = opts.service_chain
559         if opts.service_chain_count:
560             config.service_chain_count = opts.service_chain_count
561         if opts.no_vswitch_access:
562             config.no_vswitch_access = opts.no_vswitch_access
563         if opts.hypervisor:
564             # can be any of 'comp1', 'nova:', 'nova:comp1'
565             config.compute_nodes = opts.hypervisor
566         if opts.vxlan:
567             config.vxlan = True
568         if opts.restart:
569             config.restart = True
570         # port to port loopback (direct or through switch)
571         if opts.l2_loopback:
572             config.l2_loopback = True
573             if config.service_chain != ChainType.EXT:
574                 LOG.info('Changing service chain type to EXT')
575                 config.service_chain = ChainType.EXT
576             if not config.no_arp:
577                 LOG.info('Disabling ARP')
578                 config.no_arp = True
579             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
580             LOG.info('Running L2 loopback: using EXT chain/no ARP')
581
582         if opts.use_sriov_middle_net:
583             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
584                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
585             config.use_sriov_middle_net = True
586
587         if config.sriov and config.service_chain != ChainType.EXT:
588             # if sriov is requested (does not apply to ext chains)
589             # make sure the physnet names are specified
590             check_physnet("left", config.internal_networks.left)
591             check_physnet("right", config.internal_networks.right)
592             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
593                 check_physnet("middle", config.internal_networks.middle)
594
595         # show running config in json format
596         if opts.show_config:
597             print json.dumps(config, sort_keys=True, indent=4)
598             sys.exit(0)
599
600         # update the config in the config plugin as it might have changed
601         # in a copy of the dict (config plugin still holds the original dict)
602         config_plugin.set_config(config)
603
604         if opts.status or opts.cleanup or opts.force_cleanup:
605             status_cleanup(config, opts.cleanup, opts.force_cleanup)
606
607         # add file log if requested
608         if config.log_file:
609             log.add_file_logger(config.log_file)
610
611         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
612             else None
613
614         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
615
616         if opts.server:
617             server = WebServer(nfvbench_instance, fluent_logger)
618             try:
619                 port = int(opts.port)
620             except ValueError:
621                 server.run(host=opts.host)
622             else:
623                 server.run(host=opts.host, port=port)
624             # server.run() should never return
625         else:
626             with utils.RunLock():
627                 run_summary_required = True
628                 if unknown_opts:
629                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
630                     LOG.error(err_msg)
631                     raise Exception(err_msg)
632
633                 # remove unfilled values
634                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
635                 # get CLI args
636                 params = ' '.join(str(e) for e in sys.argv[1:])
637                 result = nfvbench_instance.run(opts, params)
638                 if 'error_message' in result:
639                     raise Exception(result['error_message'])
640
641                 if 'result' in result and result['status']:
642                     nfvbench_instance.save(result['result'])
643                     nfvbench_instance.prepare_summary(result['result'])
644     except Exception as exc:
645         run_summary_required = True
646         LOG.error({
647             'status': NFVBench.STATUS_ERROR,
648             'error_message': traceback.format_exc()
649         })
650         print str(exc)
651     finally:
652         if fluent_logger:
653             # only send a summary record if there was an actual nfvbench run or
654             # if an error/exception was logged.
655             fluent_logger.send_run_summary(run_summary_required)
656
657
658 if __name__ == '__main__':
659     main()