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