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