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