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