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