NFVBENCH-35 Runlogdate is 0 in resultnfvbench
[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 prepare_summary(self, result):
131         """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
132         global fluent_logger
133         sender = None
134         if fluent_logger:
135             sender = FluentLogHandler("resultnfvbench",
136                                       fluentd_ip=self.config.fluentd.ip,
137                                       fluentd_port=self.config.fluentd.port)
138             sender.runlogdate = fluent_logger.runlogdate
139         summary = NFVBenchSummarizer(result, sender)
140         LOG.info(str(summary))
141
142     def save(self, result):
143         """Save results in json format file."""
144         utils.save_json_result(result,
145                                self.config.json_file,
146                                self.config.std_json_path,
147                                self.config.service_chain,
148                                self.config.service_chain_count,
149                                self.config.flow_count,
150                                self.config.frame_sizes)
151
152     def update_config(self, opts):
153         self.config = AttrDict(dict(self.base_config))
154         self.config.update(opts)
155
156         self.config.service_chain = self.config.service_chain.upper()
157         self.config.service_chain_count = int(self.config.service_chain_count)
158         self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
159         required_flow_count = self.config.service_chain_count * 2
160         if self.config.flow_count < required_flow_count:
161             LOG.info("Flow count '{}' has been set to minimum value of '{}' "
162                      "for current configuration".format(self.config.flow_count,
163                                                         required_flow_count))
164             self.config.flow_count = required_flow_count
165
166         if self.config.flow_count % 2 != 0:
167             self.config.flow_count += 1
168
169         self.config.duration_sec = float(self.config.duration_sec)
170         self.config.interval_sec = float(self.config.interval_sec)
171
172         # Get traffic generator profile config
173         if not self.config.generator_profile:
174             self.config.generator_profile = self.config.traffic_generator.default_profile
175
176         generator_factory = TrafficGeneratorFactory(self.config)
177         self.config.generator_config = \
178             generator_factory.get_generator_config(self.config.generator_profile)
179
180         if not any(self.config.generator_config.pcis):
181             raise Exception("PCI addresses configuration for selected traffic generator profile "
182                             "({tg_profile}) are missing. Please specify them in configuration file."
183                             .format(tg_profile=self.config.generator_profile))
184
185         if self.config.traffic is None or len(self.config.traffic) == 0:
186             raise Exception("No traffic profile found in traffic configuration, "
187                             "please fill 'traffic' section in configuration file.")
188
189         if isinstance(self.config.traffic, tuple):
190             self.config.traffic = self.config.traffic[0]
191
192         self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
193
194         self.config.ipv6_mode = False
195         self.config.no_dhcp = True
196         self.config.same_network_only = True
197         if self.config.openrc_file:
198             self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
199
200         self.config.ndr_run = (not self.config.no_traffic
201                                and 'ndr' in self.config.rate.strip().lower().split('_'))
202         self.config.pdr_run = (not self.config.no_traffic
203                                and 'pdr' in self.config.rate.strip().lower().split('_'))
204         self.config.single_run = (not self.config.no_traffic
205                                   and not (self.config.ndr_run or self.config.pdr_run))
206
207         if self.config.vlans and len(self.config.vlans) != 2:
208             raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
209
210         self.config.json_file = self.config.json if self.config.json else None
211         if self.config.json_file:
212             (path, filename) = os.path.split(self.config.json)
213             if not os.path.exists(path):
214                 raise Exception('Please provide existing path for storing results in JSON file. '
215                                 'Path used: {path}'.format(path=path))
216
217         self.config.std_json_path = self.config.std_json if self.config.std_json else None
218         if self.config.std_json_path:
219             if not os.path.exists(self.config.std_json):
220                 raise Exception('Please provide existing path for storing results in JSON file. '
221                                 'Path used: {path}'.format(path=self.config.std_json_path))
222
223         self.config_plugin.validate_config(self.config, self.specs.openstack)
224
225
226 def parse_opts_from_cli():
227     parser = argparse.ArgumentParser()
228
229     parser.add_argument('-c', '--config', dest='config',
230                         action='store',
231                         help='Override default values with a config file or '
232                              'a yaml/json config string',
233                         metavar='<file_name_or_yaml>')
234
235     parser.add_argument('--server', dest='server',
236                         default=None,
237                         action='store',
238                         metavar='<http_root_pathname>',
239                         help='Run nfvbench in server mode and pass'
240                              ' the HTTP root folder full pathname')
241
242     parser.add_argument('--host', dest='host',
243                         action='store',
244                         default='0.0.0.0',
245                         help='Host IP address on which server will be listening (default 0.0.0.0)')
246
247     parser.add_argument('-p', '--port', dest='port',
248                         action='store',
249                         default=7555,
250                         help='Port on which server will be listening (default 7555)')
251
252     parser.add_argument('-sc', '--service-chain', dest='service_chain',
253                         choices=BasicFactory.chain_classes,
254                         action='store',
255                         help='Service chain to run')
256
257     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
258                         action='store',
259                         help='Set number of service chains to run',
260                         metavar='<service_chain_count>')
261
262     parser.add_argument('-fc', '--flow-count', dest='flow_count',
263                         action='store',
264                         help='Set number of total flows for all chains and all directions',
265                         metavar='<flow_count>')
266
267     parser.add_argument('--rate', dest='rate',
268                         action='store',
269                         help='Specify rate in pps, bps or %% as total for all directions',
270                         metavar='<rate>')
271
272     parser.add_argument('--duration', dest='duration_sec',
273                         action='store',
274                         help='Set duration to run traffic generator (in seconds)',
275                         metavar='<duration_sec>')
276
277     parser.add_argument('--interval', dest='interval_sec',
278                         action='store',
279                         help='Set interval to record traffic generator stats (in seconds)',
280                         metavar='<interval_sec>')
281
282     parser.add_argument('--inter-node', dest='inter_node',
283                         default=None,
284                         action='store_true',
285                         help='run VMs in different compute nodes (PVVP only)')
286
287     parser.add_argument('--sriov', dest='sriov',
288                         default=None,
289                         action='store_true',
290                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
291
292     parser.add_argument('-d', '--debug', dest='debug',
293                         action='store_true',
294                         default=None,
295                         help='print debug messages (verbose)')
296
297     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
298                         action='store',
299                         help='Traffic generator profile to use')
300
301
302     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
303                         default=None,
304                         action='store_true',
305                         help='Check config and connectivity only - do not generate traffic')
306
307     parser.add_argument('--no-arp', dest='no_arp',
308                         default=None,
309                         action='store_true',
310                         help='Do not use ARP to find MAC addresses, '
311                              'instead use values in config file')
312
313     parser.add_argument('--no-reset', dest='no_reset',
314                         default=None,
315                         action='store_true',
316                         help='Do not reset counters prior to running')
317
318     parser.add_argument('--no-int-config', dest='no_int_config',
319                         default=None,
320                         action='store_true',
321                         help='Skip interfaces config on EXT service chain')
322
323     parser.add_argument('--no-tor-access', dest='no_tor_access',
324                         default=None,
325                         action='store_true',
326                         help='Skip TOR switch configuration and retrieving of stats')
327
328     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
329                         default=None,
330                         action='store_true',
331                         help='Skip vswitch configuration and retrieving of stats')
332
333     parser.add_argument('--no-cleanup', dest='no_cleanup',
334                         default=None,
335                         action='store_true',
336                         help='no cleanup after run')
337
338     parser.add_argument('--json', dest='json',
339                         action='store',
340                         help='store results in json format file',
341                         metavar='<path>/<filename>')
342
343     parser.add_argument('--std-json', dest='std_json',
344                         action='store',
345                         help='store results in json format file with nfvbench standard filename: '
346                              '<service-chain-type>-<service-chain-count>-<flow-count>'
347                              '-<packet-sizes>.json',
348                         metavar='<path>')
349
350     parser.add_argument('--show-default-config', dest='show_default_config',
351                         default=None,
352                         action='store_true',
353                         help='print the default config in yaml format (unedited)')
354
355     parser.add_argument('--show-config', dest='show_config',
356                         default=None,
357                         action='store_true',
358                         help='print the running config in json format')
359
360     parser.add_argument('-ss', '--show-summary', dest='summary',
361                         action='store',
362                         help='Show summary from nfvbench json file',
363                         metavar='<json>')
364
365     parser.add_argument('-v', '--version', dest='version',
366                         default=None,
367                         action='store_true',
368                         help='Show version')
369
370     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
371                         action='append',
372                         help='Override traffic profile frame sizes',
373                         metavar='<frame_size_bytes or IMIX>')
374
375     parser.add_argument('--unidir', dest='unidir',
376                         action='store_true',
377                         default=None,
378                         help='Override traffic profile direction (requires -fs)')
379
380     parser.add_argument('--log-file', '--logfile', dest='log_file',
381                         action='store',
382                         help='Filename for saving logs',
383                         metavar='<log_file>'),
384
385     parser.add_argument('--user-label', '--userlabel', dest='user_label',
386                         action='store',
387                         help='Custom label for performance records')
388
389     opts, unknown_opts = parser.parse_known_args()
390     return opts, unknown_opts
391
392
393 def load_default_config():
394     default_cfg = resource_string(__name__, "cfg.default.yaml")
395     config = config_loads(default_cfg)
396     config.name = '(built-in default config)'
397     return config, default_cfg
398
399
400 def override_custom_traffic(config, frame_sizes, unidir):
401     """Override the traffic profiles with a custom one
402     """
403     if frame_sizes is not None:
404         traffic_profile_name = "custom_traffic_profile"
405         config.traffic_profile = [
406             {
407                 "l2frame_size": frame_sizes,
408                 "name": traffic_profile_name
409             }
410         ]
411     else:
412         traffic_profile_name = config.traffic["profile"]
413
414     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
415     config.traffic = {
416         "bidirectional": bidirectional,
417         "profile": traffic_profile_name
418     }
419
420
421 def check_physnet(name, netattrs):
422     if not netattrs.physical_network:
423         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
424                         .format(n=name))
425     if not netattrs.segmentation_id:
426         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
427                         .format(n=name))
428
429
430 def main():
431     global fluent_logger
432     run_summary_required = False
433     try:
434         log.setup()
435         # load default config file
436         config, default_cfg = load_default_config()
437         # create factory for platform specific classes
438         try:
439             factory_module = importlib.import_module(config['factory_module'])
440             factory = getattr(factory_module, config['factory_class'])()
441         except AttributeError:
442             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
443                             .format(m=config['factory_module'], c=config['factory_class']))
444         # create config plugin for this platform
445         config_plugin = factory.get_config_plugin_class()(config)
446         config = config_plugin.get_config()
447         openstack_spec = config_plugin.get_openstack_spec()
448
449         # setup the fluent logger as soon as possible right after the config plugin is called
450         if config.fluentd.logging_tag:
451             fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
452                                              fluentd_ip=config.fluentd.ip,
453                                              fluentd_port=config.fluentd.port)
454             LOG.addHandler(fluent_logger)
455         else:
456             fluent_logger = None
457
458         opts, unknown_opts = parse_opts_from_cli()
459         log.set_level(debug=opts.debug)
460
461         if opts.version:
462             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
463             sys.exit(0)
464
465         if opts.summary:
466             with open(opts.summary) as json_data:
467                 print NFVBenchSummarizer(json.load(json_data), None)
468             sys.exit(0)
469
470         # show default config in text/yaml format
471         if opts.show_default_config:
472             print default_cfg
473             sys.exit(0)
474
475         config.name = ''
476         if opts.config:
477             # do not check extra_specs in flavor as it can contain any key/value pairs
478             whitelist_keys = ['extra_specs']
479             # override default config options with start config at path parsed from CLI
480             # check if it is an inline yaml/json config or a file name
481             if os.path.isfile(opts.config):
482                 LOG.info('Loading configuration file: ' + opts.config)
483                 config = config_load(opts.config, config, whitelist_keys)
484                 config.name = os.path.basename(opts.config)
485             else:
486                 LOG.info('Loading configuration string: ' + opts.config)
487                 config = config_loads(opts.config, config, whitelist_keys)
488
489         # traffic profile override options
490         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
491
492         # copy over cli options that are used in config
493         config.generator_profile = opts.generator_profile
494         if opts.sriov:
495             config.sriov = True
496         if opts.log_file:
497             config.log_file = opts.log_file
498
499         # show running config in json format
500         if opts.show_config:
501             print json.dumps(config, sort_keys=True, indent=4)
502             sys.exit(0)
503
504         if config.sriov and config.service_chain != ChainType.EXT:
505             # if sriov is requested (does not apply to ext chains)
506             # make sure the physnet names are specified
507             check_physnet("left", config.internal_networks.left)
508             check_physnet("right", config.internal_networks.right)
509             if config.service_chain == ChainType.PVVP:
510                 check_physnet("middle", config.internal_networks.middle)
511
512         # update the config in the config plugin as it might have changed
513         # in a copy of the dict (config plugin still holds the original dict)
514         config_plugin.set_config(config)
515
516         # add file log if requested
517         if config.log_file:
518             log.add_file_logger(config.log_file)
519
520         nfvbench = NFVBench(config, openstack_spec, config_plugin, factory)
521
522         if opts.server:
523             if os.path.isdir(opts.server):
524                 server = WebSocketIoServer(opts.server, nfvbench, fluent_logger)
525                 nfvbench.set_notifier(server)
526                 try:
527                     port = int(opts.port)
528                 except ValueError:
529                     server.run(host=opts.host)
530                 else:
531                     server.run(host=opts.host, port=port)
532             else:
533                 print 'Invalid HTTP root directory: ' + opts.server
534                 sys.exit(1)
535         else:
536             with utils.RunLock():
537                 run_summary_required = True
538                 if unknown_opts:
539                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
540                     LOG.error(err_msg)
541                     raise Exception(err_msg)
542
543                 # remove unfilled values
544                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
545                 # get CLI args
546                 params = ' '.join(str(e) for e in sys.argv[1:])
547                 result = nfvbench.run(opts, params)
548                 if 'error_message' in result:
549                     raise Exception(result['error_message'])
550
551                 if 'result' in result and result['status']:
552                     nfvbench.save(result['result'])
553                     nfvbench.prepare_summary(result['result'])
554     except Exception as exc:
555         run_summary_required = True
556         LOG.error({
557             'status': NFVBench.STATUS_ERROR,
558             'error_message': traceback.format_exc()
559         })
560         print str(exc)
561     finally:
562         if fluent_logger:
563             # only send a summary record if there was an actual nfvbench run or
564             # if an error/exception was logged.
565             fluent_logger.send_run_summary(run_summary_required)
566
567
568 if __name__ == '__main__':
569     main()