[NFVBENCH-58] Add option to specify dest MAC with EXT CHAIN when ARP is
[nfvbench.git] / nfvbench / nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16
17 import argparse
18 from collections import defaultdict
19 import copy
20 import datetime
21 import importlib
22 import json
23 import os
24 import sys
25 import traceback
26
27 from attrdict import AttrDict
28 import pbr.version
29 from pkg_resources import resource_string
30
31 from __init__ import __version__
32 from chain_runner import ChainRunner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from factory import BasicFactory
37 from fluentd import FluentLogHandler
38 import log
39 from log import LOG
40 from nfvbenchd import WebSocketIoServer
41 from specs import ChainType
42 from specs import Specs
43 from summarizer import NFVBenchSummarizer
44 from traffic_client import TrafficGeneratorFactory
45 import utils
46
47 fluent_logger = None
48
49
50 class NFVBench(object):
51     """Main class of NFV benchmarking tool."""
52     STATUS_OK = 'OK'
53     STATUS_ERROR = 'ERROR'
54
55     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56         self.base_config = config
57         self.config = None
58         self.config_plugin = config_plugin
59         self.factory = factory
60         self.notifier = notifier
61         self.cred = credentials.Credentials(config.openrc_file, None, False) \
62             if config.openrc_file else None
63         self.chain_runner = None
64         self.specs = Specs()
65         self.specs.set_openstack_spec(openstack_spec)
66         self.clients = defaultdict(lambda: None)
67         self.vni_ports = []
68         sys.stdout.flush()
69
70     def setup(self):
71         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
72         self.chain_runner = ChainRunner(self.config,
73                                         self.clients,
74                                         self.cred,
75                                         self.specs,
76                                         self.factory,
77                                         self.notifier)
78
79     def set_notifier(self, notifier):
80         self.notifier = notifier
81
82     def run(self, opts, args):
83         status = NFVBench.STATUS_OK
84         result = None
85         message = ''
86         if fluent_logger:
87             # take a snapshot of the current time for this new run
88             # so that all subsequent logs can relate to this run
89             fluent_logger.start_new_run()
90         LOG.info(args)
91         try:
92             self.update_config(opts)
93             self.setup()
94
95             result = {
96                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
97                 "nfvbench_version": __version__,
98                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
99                 "benchmarks": {
100                     "network": {
101                         "service_chain": self.chain_runner.run(),
102                         "versions": self.chain_runner.get_version(),
103                     }
104                 }
105             }
106             if self.specs.openstack:
107                 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
108                                             "encaps": self.specs.openstack.encaps}
109             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
110         except Exception:
111             status = NFVBench.STATUS_ERROR
112             message = traceback.format_exc()
113         except KeyboardInterrupt:
114             status = NFVBench.STATUS_ERROR
115             message = traceback.format_exc()
116         finally:
117             if self.chain_runner:
118                 self.chain_runner.close()
119
120         if status == NFVBench.STATUS_OK:
121             result = utils.dict_to_json_dict(result)
122             return {
123                 'status': status,
124                 'result': result
125             }
126         return {
127             'status': status,
128             'error_message': message
129         }
130
131     def prepare_summary(self, result):
132         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
133         global fluent_logger
134         summary = NFVBenchSummarizer(result, fluent_logger)
135         LOG.info(str(summary))
136
137     def save(self, result):
138         """Save results in json format file."""
139         utils.save_json_result(result,
140                                self.config.json_file,
141                                self.config.std_json_path,
142                                self.config.service_chain,
143                                self.config.service_chain_count,
144                                self.config.flow_count,
145                                self.config.frame_sizes)
146
147     def update_config(self, opts):
148         self.config = AttrDict(dict(self.base_config))
149         self.config.update(opts)
150
151         self.config.service_chain = self.config.service_chain.upper()
152         self.config.service_chain_count = int(self.config.service_chain_count)
153         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
154         required_flow_count = self.config.service_chain_count * 2
155         if self.config.flow_count < required_flow_count:
156             LOG.info("Flow count %d has been set to minimum value of '%d' "
157                      "for current configuration", self.config.flow_count,
158                      required_flow_count)
159             self.config.flow_count = required_flow_count
160
161         if self.config.flow_count % 2 != 0:
162             self.config.flow_count += 1
163
164         self.config.duration_sec = float(self.config.duration_sec)
165         self.config.interval_sec = float(self.config.interval_sec)
166
167         # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
168         if self.config.service_chain == ChainType.EXT and self.config.no_arp:
169             if not (self.config.generator_config.mac_addrs_left is None and
170                     self.config.generator_config.mac_addrs_right is None):
171                 if (self.config.generator_config.mac_addrs_left is None or
172                         self.config.generator_config.mac_addrs_right is None):
173                     raise Exception("mac_addrs_left and mac_addrs_right must either "
174                                     "both be None or have a number of entries matching "
175                                     "service_chain_count")
176                 if not (len(self.config.generator_config.mac_addrs_left) ==
177                         self.config.service_chain_count and
178                         len(self.config.generator_config.mac_addrs_right) ==
179                         self.config.service_chain_count):
180                     raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
181                                     "does not match service_chain_count ({c})"
182                                     .format(a=len(self.config.generator_config.mac_addrs_left),
183                                             b=len(self.config.generator_config.mac_addrs_right),
184                                             c=self.config.service_chain_count))
185
186         # Get traffic generator profile config
187         if not self.config.generator_profile:
188             self.config.generator_profile = self.config.traffic_generator.default_profile
189
190         generator_factory = TrafficGeneratorFactory(self.config)
191         self.config.generator_config = \
192             generator_factory.get_generator_config(self.config.generator_profile)
193
194         if not any(self.config.generator_config.pcis):
195             raise Exception("PCI addresses configuration for selected traffic generator profile "
196                             "({tg_profile}) are missing. Please specify them in configuration file."
197                             .format(tg_profile=self.config.generator_profile))
198
199         if self.config.traffic is None or not self.config.traffic:
200             raise Exception("No traffic profile found in traffic configuration, "
201                             "please fill 'traffic' section in configuration file.")
202
203         if isinstance(self.config.traffic, tuple):
204             self.config.traffic = self.config.traffic[0]
205
206         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
207
208         self.config.ipv6_mode = False
209         self.config.no_dhcp = True
210         self.config.same_network_only = True
211         if self.config.openrc_file:
212             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
213
214         self.config.ndr_run = (not self.config.no_traffic
215                                and 'ndr' in self.config.rate.strip().lower().split('_'))
216         self.config.pdr_run = (not self.config.no_traffic
217                                and 'pdr' in self.config.rate.strip().lower().split('_'))
218         self.config.single_run = (not self.config.no_traffic
219                                   and not (self.config.ndr_run or self.config.pdr_run))
220
221         if self.config.vlans and len(self.config.vlans) != 2:
222             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
223
224         self.config.json_file = self.config.json if self.config.json else None
225         if self.config.json_file:
226             (path, _filename) = os.path.split(self.config.json)
227             if not os.path.exists(path):
228                 raise Exception('Please provide existing path for storing results in JSON file. '
229                                 'Path used: {path}'.format(path=path))
230
231         self.config.std_json_path = self.config.std_json if self.config.std_json else None
232         if self.config.std_json_path:
233             if not os.path.exists(self.config.std_json):
234                 raise Exception('Please provide existing path for storing results in JSON file. '
235                                 'Path used: {path}'.format(path=self.config.std_json_path))
236
237         self.config_plugin.validate_config(self.config, self.specs.openstack)
238
239
240 def parse_opts_from_cli():
241     parser = argparse.ArgumentParser()
242
243     parser.add_argument('-c', '--config', dest='config',
244                         action='store',
245                         help='Override default values with a config file or '
246                              'a yaml/json config string',
247                         metavar='<file_name_or_yaml>')
248
249     parser.add_argument('--server', dest='server',
250                         default=None,
251                         action='store',
252                         metavar='<http_root_pathname>',
253                         help='Run nfvbench in server mode and pass'
254                              ' the HTTP root folder full pathname')
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=BasicFactory.chain_classes,
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='run VMs in different compute nodes (PVVP only)')
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-reset', dest='no_reset',
333                         default=None,
334                         action='store_true',
335                         help='Do not reset counters prior to running')
336
337     parser.add_argument('--no-int-config', dest='no_int_config',
338                         default=None,
339                         action='store_true',
340                         help='Skip interfaces config on EXT service chain')
341
342     parser.add_argument('--no-tor-access', dest='no_tor_access',
343                         default=None,
344                         action='store_true',
345                         help='Skip TOR switch configuration and retrieving of stats')
346
347     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
348                         default=None,
349                         action='store_true',
350                         help='Skip vswitch configuration and retrieving of stats')
351
352     parser.add_argument('--no-cleanup', dest='no_cleanup',
353                         default=None,
354                         action='store_true',
355                         help='no cleanup after run')
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     opts, unknown_opts = parser.parse_known_args()
409     return opts, unknown_opts
410
411
412 def load_default_config():
413     default_cfg = resource_string(__name__, "cfg.default.yaml")
414     config = config_loads(default_cfg)
415     config.name = '(built-in default config)'
416     return config, default_cfg
417
418
419 def override_custom_traffic(config, frame_sizes, unidir):
420     """Override the traffic profiles with a custom one
421     """
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
449 def main():
450     global fluent_logger
451     run_summary_required = False
452     try:
453         log.setup()
454         # load default config file
455         config, default_cfg = load_default_config()
456         # create factory for platform specific classes
457         try:
458             factory_module = importlib.import_module(config['factory_module'])
459             factory = getattr(factory_module, config['factory_class'])()
460         except AttributeError:
461             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
462                             .format(m=config['factory_module'], c=config['factory_class']))
463         # create config plugin for this platform
464         config_plugin = factory.get_config_plugin_class()(config)
465         config = config_plugin.get_config()
466
467         opts, unknown_opts = parse_opts_from_cli()
468         log.set_level(debug=opts.debug)
469
470         # setup the fluent logger as soon as possible right after the config plugin is called,
471         # if there is any logging or result tag is set then initialize the fluent logger
472         for fluentd in config.fluentd:
473             if fluentd.logging_tag or fluentd.result_tag:
474                 fluent_logger = FluentLogHandler(config.fluentd)
475                 LOG.addHandler(fluent_logger)
476                 break
477
478         if opts.version:
479             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
480             sys.exit(0)
481
482         if opts.summary:
483             with open(opts.summary) as json_data:
484                 result = json.load(json_data)
485                 if opts.user_label:
486                     result['config']['user_label'] = opts.user_label
487                 print NFVBenchSummarizer(result, fluent_logger)
488             sys.exit(0)
489
490         # show default config in text/yaml format
491         if opts.show_default_config:
492             print default_cfg
493             sys.exit(0)
494
495         config.name = ''
496         if opts.config:
497             # do not check extra_specs in flavor as it can contain any key/value pairs
498             whitelist_keys = ['extra_specs']
499             # override default config options with start config at path parsed from CLI
500             # check if it is an inline yaml/json config or a file name
501             if os.path.isfile(opts.config):
502                 LOG.info('Loading configuration file: %s', opts.config)
503                 config = config_load(opts.config, config, whitelist_keys)
504                 config.name = os.path.basename(opts.config)
505             else:
506                 LOG.info('Loading configuration string: %s', opts.config)
507                 config = config_loads(opts.config, config, whitelist_keys)
508
509         # traffic profile override options
510         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
511
512         # copy over cli options that are used in config
513         config.generator_profile = opts.generator_profile
514         if opts.sriov:
515             config.sriov = True
516         if opts.log_file:
517             config.log_file = opts.log_file
518         if opts.service_chain:
519             config.service_chain = opts.service_chain
520         if opts.service_chain_count:
521             config.service_chain_count = opts.service_chain_count
522
523         if opts.use_sriov_middle_net:
524             if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
525                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
526             config.use_sriov_middle_net = True
527
528         if config.sriov and config.service_chain != ChainType.EXT:
529             # if sriov is requested (does not apply to ext chains)
530             # make sure the physnet names are specified
531             check_physnet("left", config.internal_networks.left)
532             check_physnet("right", config.internal_networks.right)
533             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
534                 check_physnet("middle", config.internal_networks.middle)
535
536         # show running config in json format
537         if opts.show_config:
538             print json.dumps(config, sort_keys=True, indent=4)
539             sys.exit(0)
540
541         # check that an empty openrc file (no OpenStack) is only allowed
542         # with EXT chain
543         if not config.openrc_file:
544             if config.service_chain == ChainType.EXT:
545                 LOG.info('EXT chain with OpenStack mode disabled')
546             else:
547                 raise Exception("openrc_file is empty in the configuration and is required")
548
549         # update the config in the config plugin as it might have changed
550         # in a copy of the dict (config plugin still holds the original dict)
551         config_plugin.set_config(config)
552
553         # add file log if requested
554         if config.log_file:
555             log.add_file_logger(config.log_file)
556
557         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
558             else None
559
560         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
561
562         if opts.server:
563             if os.path.isdir(opts.server):
564                 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
565                 nfvbench_instance.set_notifier(server)
566                 try:
567                     port = int(opts.port)
568                 except ValueError:
569                     server.run(host=opts.host)
570                 else:
571                     server.run(host=opts.host, port=port)
572             else:
573                 print 'Invalid HTTP root directory: ' + opts.server
574                 sys.exit(1)
575         else:
576             with utils.RunLock():
577                 run_summary_required = True
578                 if unknown_opts:
579                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
580                     LOG.error(err_msg)
581                     raise Exception(err_msg)
582
583                 # remove unfilled values
584                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
585                 # get CLI args
586                 params = ' '.join(str(e) for e in sys.argv[1:])
587                 result = nfvbench_instance.run(opts, params)
588                 if 'error_message' in result:
589                     raise Exception(result['error_message'])
590
591                 if 'result' in result and result['status']:
592                     nfvbench_instance.save(result['result'])
593                     nfvbench_instance.prepare_summary(result['result'])
594     except Exception as exc:
595         run_summary_required = True
596         LOG.error({
597             'status': NFVBench.STATUS_ERROR,
598             'error_message': traceback.format_exc()
599         })
600         print str(exc)
601     finally:
602         if fluent_logger:
603             # only send a summary record if there was an actual nfvbench run or
604             # if an error/exception was logged.
605             fluent_logger.send_run_summary(run_summary_required)
606
607
608 if __name__ == '__main__':
609     main()