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