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