NFVBENCH-4 Add support for log file
[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         summary = NFVBenchSummarizer(result)
125         LOG.info(str(summary))
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     parser.add_argument('--log-file', '--logfile', dest='log_file',
369                         action='store',
370                         help='Filename for saving logs',
371                         metavar='<log_file>')
372
373     opts, unknown_opts = parser.parse_known_args()
374     return opts, unknown_opts
375
376
377 def load_default_config():
378     default_cfg = resource_string(__name__, "cfg.default.yaml")
379     config = config_loads(default_cfg)
380     config.name = '(built-in default config)'
381     return config, default_cfg
382
383
384 def override_custom_traffic(config, frame_sizes, unidir):
385     """Override the traffic profiles with a custom one
386     """
387     if frame_sizes is not None:
388         traffic_profile_name = "custom_traffic_profile"
389         config.traffic_profile = [
390             {
391                 "l2frame_size": frame_sizes,
392                 "name": traffic_profile_name
393             }
394         ]
395     else:
396         traffic_profile_name = config.traffic["profile"]
397
398     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
399     config.traffic = {
400         "bidirectional": bidirectional,
401         "profile": traffic_profile_name
402     }
403
404 def check_physnet(name, netattrs):
405     if not netattrs.physical_network:
406         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
407                             .format(n=name))
408     if not netattrs.segmentation_id:
409         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
410                             .format(n=name))
411
412 def main():
413     try:
414         log.setup()
415         # load default config file
416         config, default_cfg = load_default_config()
417         # create factory for platform specific classes
418         try:
419             factory_module = importlib.import_module(config['factory_module'])
420             factory = getattr(factory_module, config['factory_class'])()
421         except AttributeError:
422             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
423                             .format(m=config['factory_module'], c=config['factory_class']))
424         # create config plugin for this platform
425         config_plugin = factory.get_config_plugin_class()(config)
426         config = config_plugin.get_config()
427         openstack_spec = config_plugin.get_openstack_spec()
428
429         opts, unknown_opts = parse_opts_from_cli()
430         log.set_level(debug=opts.debug)
431
432         if opts.version:
433             print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
434             sys.exit(0)
435
436         if opts.summary:
437             with open(opts.summary) as json_data:
438                 print NFVBenchSummarizer(json.load(json_data))
439             sys.exit(0)
440
441         # show default config in text/yaml format
442         if opts.show_default_config:
443             print default_cfg
444             sys.exit(0)
445
446         config.name = ''
447         if opts.config:
448             # override default config options with start config at path parsed from CLI
449             # check if it is an inline yaml/json config or a file name
450             if os.path.isfile(opts.config):
451                 LOG.info('Loading configuration file: ' + opts.config)
452                 config = config_load(opts.config, config)
453                 config.name = os.path.basename(opts.config)
454             else:
455                 LOG.info('Loading configuration string: ' + opts.config)
456                 config = config_loads(opts.config, config)
457
458         # traffic profile override options
459         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
460
461         # copy over cli options that are used in config
462         config.generator_profile = opts.generator_profile
463         if opts.sriov:
464             config.sriov = True
465         if opts.log_file:
466             config.log_file = opts.log_file
467
468         # show running config in json format
469         if opts.show_config:
470             print json.dumps(config, sort_keys=True, indent=4)
471             sys.exit(0)
472
473         if config.sriov and config.service_chain != ChainType.EXT:
474             # if sriov is requested (does not apply to ext chains)
475             # make sure the physnet names are specified
476             check_physnet("left", config.internal_networks.left)
477             check_physnet("right", config.internal_networks.right)
478             if config.service_chain == ChainType.PVVP:
479                 check_physnet("middle", config.internal_networks.middle)
480
481         # update the config in the config plugin as it might have changed
482         # in a copy of the dict (config plugin still holds the original dict)
483         config_plugin.set_config(config)
484
485         # add file log if requested
486         if config.log_file:
487             log.add_file_logger(config.log_file)
488
489         nfvbench = NFVBench(config, openstack_spec, config_plugin, factory)
490
491         if opts.server:
492             if os.path.isdir(opts.server):
493                 server = WebSocketIoServer(opts.server, nfvbench)
494                 nfvbench.set_notifier(server)
495                 try:
496                     port = int(opts.port)
497                 except ValueError:
498                     server.run(host=opts.host)
499                 else:
500                     server.run(host=opts.host, port=port)
501             else:
502                 print 'Invalid HTTP root directory: ' + opts.server
503                 sys.exit(1)
504         else:
505             with utils.RunLock():
506                 if unknown_opts:
507                     LOG.warning('Unknown options: ' + ' '.join(unknown_opts))
508
509                 # remove unfilled values
510                 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
511                 result = nfvbench.run(opts)
512                 if 'error_message' in result:
513                     raise Exception(result['error_message'])
514
515                 if 'result' in result and result['status']:
516                     nfvbench.save(result['result'])
517                     nfvbench.print_summary(result['result'])
518     except Exception as exc:
519         LOG.error({
520             'status': NFVBench.STATUS_ERROR,
521             'error_message': traceback.format_exc()
522         })
523         print str(exc)
524         sys.exit(1)
525
526 if __name__ == '__main__':
527     main()