NFVBENCH-11 Cannot override extra_specs in flavor using -c
[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 from __init__ import __version__
18 import argparse
19 from attrdict import AttrDict
20 from chain_runner import ChainRunner
21 from collections import defaultdict
22 from config import config_load
23 from config import config_loads
24 import copy
25 import credentials
26 import datetime
27 from factory import BasicFactory
28 from fluentd import FluentLogHandler
29 import importlib
30 import json
31 import log
32 from log import LOG
33 from nfvbenchd import WebSocketIoServer
34 import os
35 import pbr.version
36 from pkg_resources import resource_string
37 from specs import ChainType
38 from specs import Specs
39 from summarizer import NFVBenchSummarizer
40 import sys
41 import traceback
42 from traffic_client import TrafficGeneratorFactory
43 import utils
44
45 fluent_logger = None
46
47 class NFVBench(object):
48     """Main class of NFV benchmarking tool."""
49     STATUS_OK = 'OK'
50     STATUS_ERROR = 'ERROR'
51
52     def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
53         self.base_config = config
54         self.config = None
55         self.config_plugin = config_plugin
56         self.factory = factory
57         self.notifier = notifier
58         self.cred = credentials.Credentials(config.openrc_file, None, False)
59         self.chain_runner = None
60         self.specs = Specs()
61         self.specs.set_openstack_spec(openstack_spec)
62         self.clients = defaultdict(lambda: None)
63         self.vni_ports = []
64         sys.stdout.flush()
65
66     def setup(self):
67         self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
68         self.chain_runner = ChainRunner(self.config,
69                                         self.clients,
70                                         self.cred,
71                                         self.specs,
72                                         self.factory,
73                                         self.notifier)
74
75     def set_notifier(self, notifier):
76         self.notifier = notifier
77
78     def run(self, opts):
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         try:
87             self.update_config(opts)
88             self.setup()
89
90             result = {
91                 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
92                 "nfvbench_version": __version__,
93                 "openstack_spec": {
94                     "vswitch": self.specs.openstack.vswitch,
95                     "encaps": self.specs.openstack.encaps
96                 },
97                 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
98                 "benchmarks": {
99                     "network": {
100                         "service_chain": self.chain_runner.run(),
101                         "versions": self.chain_runner.get_version(),
102                     }
103                 }
104             }
105             result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
106         except Exception:
107             status = NFVBench.STATUS_ERROR
108             message = traceback.format_exc()
109         except KeyboardInterrupt:
110             status = NFVBench.STATUS_ERROR
111             message = traceback.format_exc()
112         finally:
113             if self.chain_runner:
114                 self.chain_runner.close()
115
116             if status == NFVBench.STATUS_OK:
117                 result = utils.dict_to_json_dict(result)
118                 return {
119                     'status': status,
120                     'result': result
121                 }
122             else:
123                 return {
124                     'status': status,
125                     'error_message': message
126                 }
127
128     def print_summary(self, result):
129         """Print summary of the result"""
130         summary = NFVBenchSummarizer(result)
131         LOG.info(str(summary))
132
133     def save(self, result):
134         """Save results in json format file."""
135         utils.save_json_result(result,
136                                self.config.json_file,
137                                self.config.std_json_path,
138                                self.config.service_chain,
139                                self.config.service_chain_count,
140                                self.config.flow_count,
141                                self.config.frame_sizes)
142
143     def update_config(self, opts):
144         self.config = AttrDict(dict(self.base_config))
145         self.config.update(opts)
146
147         self.config.service_chain = self.config.service_chain.upper()
148         self.config.service_chain_count = int(self.config.service_chain_count)
149         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
150         required_flow_count = self.config.service_chain_count * 2
151         if self.config.flow_count < required_flow_count:
152             LOG.info("Flow count '{}' has been set to minimum value of '{}' "
153                      "for current configuration".format(self.config.flow_count,
154                                                         required_flow_count))
155             self.config.flow_count = required_flow_count
156
157         if self.config.flow_count % 2 != 0:
158             self.config.flow_count += 1
159
160         self.config.duration_sec = float(self.config.duration_sec)
161         self.config.interval_sec = float(self.config.interval_sec)
162
163         # Get traffic generator profile config
164         if not self.config.generator_profile:
165             self.config.generator_profile = self.config.traffic_generator.default_profile
166
167         generator_factory = TrafficGeneratorFactory(self.config)
168         self.config.generator_config = \
169             generator_factory.get_generator_config(self.config.generator_profile)
170
171         if not any(self.config.generator_config.pcis):
172             raise Exception("PCI addresses configuration for selected traffic generator profile "
173                             "({tg_profile}) are missing. Please specify them in configuration file."
174                             .format(tg_profile=self.config.generator_profile))
175
176         if self.config.traffic is None or len(self.config.traffic) == 0:
177             raise Exception("No traffic profile found in traffic configuration, "
178                             "please fill 'traffic' section in configuration file.")
179
180         if isinstance(self.config.traffic, tuple):
181             self.config.traffic = self.config.traffic[0]
182
183         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
184
185         self.config.ipv6_mode = False
186         self.config.no_dhcp = True
187         self.config.same_network_only = True
188         if self.config.openrc_file:
189             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
190
191         self.config.ndr_run = (not self.config.no_traffic
192                                and 'ndr' in self.config.rate.strip().lower().split('_'))
193         self.config.pdr_run = (not self.config.no_traffic
194                                and 'pdr' in self.config.rate.strip().lower().split('_'))
195         self.config.single_run = (not self.config.no_traffic
196                                   and not (self.config.ndr_run or self.config.pdr_run))
197
198         if self.config.vlans and len(self.config.vlans) != 2:
199             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
200
201         self.config.json_file = self.config.json if self.config.json else None
202         if self.config.json_file:
203             (path, filename) = os.path.split(self.config.json)
204             if not os.path.exists(path):
205                 raise Exception('Please provide existing path for storing results in JSON file. '
206                                 'Path used: {path}'.format(path=path))
207
208         self.config.std_json_path = self.config.std_json if self.config.std_json else None
209         if self.config.std_json_path:
210             if not os.path.exists(self.config.std_json):
211                 raise Exception('Please provide existing path for storing results in JSON file. '
212                                 'Path used: {path}'.format(path=self.config.std_json_path))
213
214         self.config_plugin.validate_config(self.config, self.specs.openstack)
215
216
217 def parse_opts_from_cli():
218     parser = argparse.ArgumentParser()
219
220     parser.add_argument('-c', '--config', dest='config',
221                         action='store',
222                         help='Override default values with a config file or '
223                              'a yaml/json config string',
224                         metavar='<file_name_or_yaml>')
225
226     parser.add_argument('--server', dest='server',
227                         default=None,
228                         action='store',
229                         metavar='<http_root_pathname>',
230                         help='Run nfvbench in server mode and pass'
231                              ' the HTTP root folder full pathname')
232
233     parser.add_argument('--host', dest='host',
234                         action='store',
235                         default='0.0.0.0',
236                         help='Host IP address on which server will be listening (default 0.0.0.0)')
237
238     parser.add_argument('-p', '--port', dest='port',
239                         action='store',
240                         default=7555,
241                         help='Port on which server will be listening (default 7555)')
242
243     parser.add_argument('-sc', '--service-chain', dest='service_chain',
244                         choices=BasicFactory.chain_classes,
245                         action='store',
246                         help='Service chain to run')
247
248     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
249                         action='store',
250                         help='Set number of service chains to run',
251                         metavar='<service_chain_count>')
252
253     parser.add_argument('-fc', '--flow-count', dest='flow_count',
254                         action='store',
255                         help='Set number of total flows for all chains and all directions',
256                         metavar='<flow_count>')
257
258     parser.add_argument('--rate', dest='rate',
259                         action='store',
260                         help='Specify rate in pps, bps or %% as total for all directions',
261                         metavar='<rate>')
262
263     parser.add_argument('--duration', dest='duration_sec',
264                         action='store',
265                         help='Set duration to run traffic generator (in seconds)',
266                         metavar='<duration_sec>')
267
268     parser.add_argument('--interval', dest='interval_sec',
269                         action='store',
270                         help='Set interval to record traffic generator stats (in seconds)',
271                         metavar='<interval_sec>')
272
273     parser.add_argument('--inter-node', dest='inter_node',
274                         default=None,
275                         action='store_true',
276                         help='run VMs in different compute nodes (PVVP only)')
277
278     parser.add_argument('--sriov', dest='sriov',
279                         default=None,
280                         action='store_true',
281                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
282
283     parser.add_argument('-d', '--debug', dest='debug',
284                         action='store_true',
285                         default=None,
286                         help='print debug messages (verbose)')
287
288     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
289                         action='store',
290                         help='Traffic generator profile to use')
291
292     parser.add_argument('-i', '--image', dest='image_name',
293                         action='store',
294                         help='VM image name to use')
295
296     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
297                         default=None,
298                         action='store_true',
299                         help='Check config and connectivity only - do not generate traffic')
300
301     parser.add_argument('--no-arp', dest='no_arp',
302                         default=None,
303                         action='store_true',
304                         help='Do not use ARP to find MAC addresses, '
305                              'instead use values in config file')
306
307     parser.add_argument('--no-reset', dest='no_reset',
308                         default=None,
309                         action='store_true',
310                         help='Do not reset counters prior to running')
311
312     parser.add_argument('--no-int-config', dest='no_int_config',
313                         default=None,
314                         action='store_true',
315                         help='Skip interfaces config on EXT service chain')
316
317     parser.add_argument('--no-tor-access', dest='no_tor_access',
318                         default=None,
319                         action='store_true',
320                         help='Skip TOR switch configuration and retrieving of stats')
321
322     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
323                         default=None,
324                         action='store_true',
325                         help='Skip vswitch configuration and retrieving of stats')
326
327     parser.add_argument('--no-cleanup', dest='no_cleanup',
328                         default=None,
329                         action='store_true',
330                         help='no cleanup after run')
331
332     parser.add_argument('--json', dest='json',
333                         action='store',
334                         help='store results in json format file',
335                         metavar='<path>/<filename>')
336
337     parser.add_argument('--std-json', dest='std_json',
338                         action='store',
339                         help='store results in json format file with nfvbench standard filename: '
340                              '<service-chain-type>-<service-chain-count>-<flow-count>'
341                              '-<packet-sizes>.json',
342                         metavar='<path>')
343
344     parser.add_argument('--show-default-config', dest='show_default_config',
345                         default=None,
346                         action='store_true',
347                         help='print the default config in yaml format (unedited)')
348
349     parser.add_argument('--show-config', dest='show_config',
350                         default=None,
351                         action='store_true',
352                         help='print the running config in json format')
353
354     parser.add_argument('-ss', '--show-summary', dest='summary',
355                         action='store',
356                         help='Show summary from nfvbench json file',
357                         metavar='<json>')
358
359     parser.add_argument('-v', '--version', dest='version',
360                         default=None,
361                         action='store_true',
362                         help='Show version')
363
364     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
365                         action='append',
366                         help='Override traffic profile frame sizes',
367                         metavar='<frame_size_bytes or IMIX>')
368
369     parser.add_argument('--unidir', dest='unidir',
370                         action='store_true',
371                         default=None,
372                         help='Override traffic profile direction (requires -fs)')
373
374     parser.add_argument('--log-file', '--logfile', dest='log_file',
375                         action='store',
376                         help='Filename for saving logs',
377                         metavar='<log_file>')
378
379     opts, unknown_opts = parser.parse_known_args()
380     return opts, unknown_opts
381
382
383 def load_default_config():
384     default_cfg = resource_string(__name__, "cfg.default.yaml")
385     config = config_loads(default_cfg)
386     config.name = '(built-in default config)'
387     return config, default_cfg
388
389
390 def override_custom_traffic(config, frame_sizes, unidir):
391     """Override the traffic profiles with a custom one
392     """
393     if frame_sizes is not None:
394         traffic_profile_name = "custom_traffic_profile"
395         config.traffic_profile = [
396             {
397                 "l2frame_size": frame_sizes,
398                 "name": traffic_profile_name
399             }
400         ]
401     else:
402         traffic_profile_name = config.traffic["profile"]
403
404     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
405     config.traffic = {
406         "bidirectional": bidirectional,
407         "profile": traffic_profile_name
408     }
409
410 def check_physnet(name, netattrs):
411     if not netattrs.physical_network:
412         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
413                         .format(n=name))
414     if not netattrs.segmentation_id:
415         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
416                         .format(n=name))
417
418 def main():
419     global fluent_logger
420     try:
421         log.setup()
422         # load default config file
423         config, default_cfg = load_default_config()
424         # create factory for platform specific classes
425         try:
426             factory_module = importlib.import_module(config['factory_module'])
427             factory = getattr(factory_module, config['factory_class'])()
428         except AttributeError:
429             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
430                             .format(m=config['factory_module'], c=config['factory_class']))
431         # create config plugin for this platform
432         config_plugin = factory.get_config_plugin_class()(config)
433         config = config_plugin.get_config()
434         openstack_spec = config_plugin.get_openstack_spec()
435
436         # setup the fluent logger as soon as possible right after the config plugin is called
437         if config.fluentd.logging_tag:
438             fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
439                                              fluentd_ip=config.fluentd.ip,
440                                              fluentd_port=config.fluentd.port)
441             LOG.addHandler(fluent_logger)
442         else:
443             fluent_logger = None
444
445         opts, unknown_opts = parse_opts_from_cli()
446         log.set_level(debug=opts.debug)
447
448         if opts.version:
449             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
450             sys.exit(0)
451
452         if opts.summary:
453             with open(opts.summary) as json_data:
454                 print NFVBenchSummarizer(json.load(json_data))
455             sys.exit(0)
456
457         # show default config in text/yaml format
458         if opts.show_default_config:
459             print default_cfg
460             sys.exit(0)
461
462         config.name = ''
463         if opts.config:
464             # do not check extra_specs in flavor as it can contain any key/value pairs
465             whitelist_keys = ['extra_specs']
466             # override default config options with start config at path parsed from CLI
467             # check if it is an inline yaml/json config or a file name
468             if os.path.isfile(opts.config):
469                 LOG.info('Loading configuration file: ' + opts.config)
470                 config = config_load(opts.config, config, whitelist_keys)
471                 config.name = os.path.basename(opts.config)
472             else:
473                 LOG.info('Loading configuration string: ' + opts.config)
474                 config = config_loads(opts.config, config, whitelist_keys)
475
476         # traffic profile override options
477         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
478
479         # copy over cli options that are used in config
480         config.generator_profile = opts.generator_profile
481         if opts.sriov:
482             config.sriov = True
483         if opts.log_file:
484             config.log_file = opts.log_file
485
486         # show running config in json format
487         if opts.show_config:
488             print json.dumps(config, sort_keys=True, indent=4)
489             sys.exit(0)
490
491         if config.sriov and config.service_chain != ChainType.EXT:
492             # if sriov is requested (does not apply to ext chains)
493             # make sure the physnet names are specified
494             check_physnet("left", config.internal_networks.left)
495             check_physnet("right", config.internal_networks.right)
496             if config.service_chain == ChainType.PVVP:
497                 check_physnet("middle", config.internal_networks.middle)
498
499         # update the config in the config plugin as it might have changed
500         # in a copy of the dict (config plugin still holds the original dict)
501         config_plugin.set_config(config)
502
503         # add file log if requested
504         if config.log_file:
505             log.add_file_logger(config.log_file)
506
507         nfvbench = NFVBench(config, openstack_spec, config_plugin, factory)
508
509         if opts.server:
510             if os.path.isdir(opts.server):
511                 server = WebSocketIoServer(opts.server, nfvbench)
512                 nfvbench.set_notifier(server)
513                 try:
514                     port = int(opts.port)
515                 except ValueError:
516                     server.run(host=opts.host)
517                 else:
518                     server.run(host=opts.host, port=port)
519             else:
520                 print 'Invalid HTTP root directory: ' + opts.server
521                 sys.exit(1)
522         else:
523             with utils.RunLock():
524                 if unknown_opts:
525                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
526                     LOG.error(err_msg)
527                     raise Exception(err_msg)
528
529                 # remove unfilled values
530                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
531                 result = nfvbench.run(opts)
532                 if 'error_message' in result:
533                     raise Exception(result['error_message'])
534
535                 if 'result' in result and result['status']:
536                     nfvbench.save(result['result'])
537                     nfvbench.print_summary(result['result'])
538     except Exception as exc:
539         LOG.error({
540             'status': NFVBench.STATUS_ERROR,
541             'error_message': traceback.format_exc()
542         })
543         print str(exc)
544         sys.exit(1)
545
546 if __name__ == '__main__':
547     main()