aboutsummaryrefslogtreecommitdiffstats
path: root/nfvbench/nfvbench.py
diff options
context:
space:
mode:
Diffstat (limited to 'nfvbench/nfvbench.py')
-rw-r--r--nfvbench/nfvbench.py452
1 files changed, 381 insertions, 71 deletions
diff --git a/nfvbench/nfvbench.py b/nfvbench/nfvbench.py
index 4a2a285..891b2bb 100644
--- a/nfvbench/nfvbench.py
+++ b/nfvbench/nfvbench.py
@@ -24,23 +24,24 @@ import sys
import traceback
from attrdict import AttrDict
+from logging import FileHandler
import pbr.version
from pkg_resources import resource_string
-from __init__ import __version__
-from chain_runner import ChainRunner
-from cleanup import Cleaner
-from config import config_load
-from config import config_loads
-import credentials as credentials
-from fluentd import FluentLogHandler
-import log
-from log import LOG
-from nfvbenchd import WebServer
-from specs import ChainType
-from specs import Specs
-from summarizer import NFVBenchSummarizer
-import utils
+from .__init__ import __version__
+from .chain_runner import ChainRunner
+from .cleanup import Cleaner
+from .config import config_load
+from .config import config_loads
+from . import credentials
+from .fluentd import FluentLogHandler
+from . import log
+from .log import LOG
+from .nfvbenchd import WebServer
+from .specs import ChainType
+from .specs import Specs
+from .summarizer import NFVBenchSummarizer
+from . import utils
fluent_logger = None
@@ -59,8 +60,8 @@ class NFVBench(object):
self.config_plugin = config_plugin
self.factory = factory
self.notifier = notifier
- self.cred = credentials.Credentials(config.openrc_file, None, False) \
- if config.openrc_file else None
+ self.cred = credentials.Credentials(config.openrc_file, config.clouds_detail, None, False) \
+ if config.openrc_file or config.clouds_detail else None
self.chain_runner = None
self.specs = Specs()
self.specs.set_openstack_spec(openstack_spec)
@@ -70,11 +71,12 @@ class NFVBench(object):
def set_notifier(self, notifier):
self.notifier = notifier
- def run(self, opts, args):
+ def run(self, opts, args, dry_run=False):
"""This run() method is called for every NFVbench benchmark request.
In CLI mode, this method is called only once per invocation.
In REST server mode, this is called once per REST POST request
+ On dry_run, show the running config in json format then exit
"""
status = NFVBench.STATUS_OK
result = None
@@ -88,10 +90,16 @@ class NFVBench(object):
# recalc the running config based on the base config and options for this run
self._update_config(opts)
+ if dry_run:
+ print((json.dumps(self.config, sort_keys=True, indent=4)))
+ sys.exit(0)
+
# check that an empty openrc file (no OpenStack) is only allowed
# with EXT chain
- if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
- raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
+ if (not self.config.openrc_file and not self.config.clouds_detail) and \
+ self.config.service_chain != ChainType.EXT:
+ raise Exception("openrc_file or clouds_detail in the configuration is required"
+ " for PVP/PVVP chains")
self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
self.specs.openstack))
@@ -164,7 +172,9 @@ class NFVBench(object):
self.config.service_chain,
self.config.service_chain_count,
self.config.flow_count,
- self.config.frame_sizes)
+ self.config.frame_sizes,
+ self.config.user_id,
+ self.config.group_id)
def _update_config(self, opts):
"""Recalculate the running config based on the base config and opts.
@@ -172,17 +182,56 @@ class NFVBench(object):
Sanity check on the config is done here as well.
"""
self.config = AttrDict(dict(self.base_config))
+ # Update log file handler if needed after a config update (REST mode)
+ if 'log_file' in opts:
+ if opts['log_file']:
+ (path, _filename) = os.path.split(opts['log_file'])
+ if not os.path.exists(path):
+ LOG.warning(
+ 'Path %s does not exist. Please verify root path is shared with host. Path '
+ 'will be created.', path)
+ os.makedirs(path)
+ LOG.info('%s is created.', path)
+ if not any(isinstance(h, FileHandler) for h in log.getLogger().handlers):
+ log.add_file_logger(opts['log_file'])
+ else:
+ for h in log.getLogger().handlers:
+ if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
+ # clean log file handler
+ log.getLogger().removeHandler(h)
+ log.add_file_logger(opts['log_file'])
+
self.config.update(opts)
config = self.config
config.service_chain = config.service_chain.upper()
config.service_chain_count = int(config.service_chain_count)
if config.l2_loopback:
- # force the number of chains to be 1 in case of l2 loopback
- config.service_chain_count = 1
+ # force the number of chains to be 1 in case of untagged l2 loopback
+ # (on the other hand, multiple L2 vlan tagged service chains are allowed)
+ if not config.vlan_tagging:
+ config.service_chain_count = 1
config.service_chain = ChainType.EXT
config.no_arp = True
LOG.info('Running L2 loopback: using EXT chain/no ARP')
+
+ # allow oversized vlan lists, just clip them
+ try:
+ vlans = [list(v) for v in config.vlans]
+ for v in vlans:
+ del v[config.service_chain_count:]
+ config.vlans = vlans
+ except Exception:
+ pass
+
+ # traffic profile override options
+ if 'frame_sizes' in opts:
+ unidir = False
+ if 'unidir' in opts:
+ unidir = opts['unidir']
+ override_custom_traffic(config, opts['frame_sizes'], unidir)
+ LOG.info("Frame size has been set to %s for current configuration", opts['frame_sizes'])
+
config.flow_count = utils.parse_flow_count(config.flow_count)
required_flow_count = config.service_chain_count * 2
if config.flow_count < required_flow_count:
@@ -194,6 +243,13 @@ class NFVBench(object):
if config.flow_count % 2:
config.flow_count += 1
+ # Possibly adjust the cache size
+ if config.cache_size < 0:
+ config.cache_size = config.flow_count
+
+ # The size must be capped to 10000 (where does this limit come from?)
+ config.cache_size = min(config.cache_size, 10000)
+
config.duration_sec = float(config.duration_sec)
config.interval_sec = float(config.interval_sec)
config.pause_sec = float(config.pause_sec)
@@ -206,7 +262,6 @@ class NFVBench(object):
if config.flavor.vcpus < 2:
raise Exception("Flavor vcpus must be >= 2")
-
config.ndr_run = (not config.no_traffic and
'ndr' in config.rate.strip().lower().split('_'))
config.pdr_run = (not config.no_traffic and
@@ -232,16 +287,36 @@ class NFVBench(object):
raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
config.vif_multiqueue_size)
- # VxLAN sanity checks
- if config.vxlan:
+ # VxLAN and MPLS sanity checks
+ if config.vxlan or config.mpls:
if config.vlan_tagging:
config.vlan_tagging = False
- LOG.info('VxLAN: vlan_tagging forced to False '
+ config.no_latency_streams = True
+ config.no_latency_stats = True
+ config.no_flow_stats = True
+ LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
'(inner VLAN tagging must be disabled)')
self.config_plugin.validate_config(config, self.specs.openstack)
+def bool_arg(x):
+ """Argument type to be used in parser.add_argument()
+ When a boolean like value is expected to be given
+ """
+ return (str(x).lower() != 'false') \
+ and (str(x).lower() != 'no') \
+ and (str(x).lower() != '0')
+
+
+def int_arg(x):
+ """Argument type to be used in parser.add_argument()
+ When an integer type value is expected to be given
+ (returns 0 if argument is invalid, hexa accepted)
+ """
+ return int(x, 0)
+
+
def _parse_opts_from_cli():
parser = argparse.ArgumentParser()
@@ -331,6 +406,12 @@ def _parse_opts_from_cli():
action='store_true',
help='Use L3 neutron routers to handle traffic')
+ parser.add_argument('-garp', '--gratuitous-arp', dest='periodic_gratuitous_arp',
+ default=None,
+ action='store_true',
+ help='Use gratuitous ARP to maintain session between TG '
+ 'and L3 routers to handle traffic')
+
parser.add_argument('-0', '--no-traffic', dest='no_traffic',
default=None,
action='store_true',
@@ -342,6 +423,12 @@ def _parse_opts_from_cli():
help='Do not use ARP to find MAC addresses, '
'instead use values in config file')
+ parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
+ default=None,
+ action='store_true',
+ help='Use ARP to find MAC addresses '
+ 'instead of using values from TRex ports (VPP forwarder only)')
+
parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
default=None,
action='store_true',
@@ -352,6 +439,11 @@ def _parse_opts_from_cli():
action='store_true',
help='Enable VxLan encapsulation')
+ parser.add_argument('--mpls', dest='mpls',
+ default=None,
+ action='store_true',
+ help='Enable MPLS encapsulation')
+
parser.add_argument('--no-cleanup', dest='no_cleanup',
default=None,
action='store_true',
@@ -389,10 +481,15 @@ def _parse_opts_from_cli():
action='store_true',
help='print the default config in yaml format (unedited)')
+ parser.add_argument('--show-pre-config', dest='show_pre_config',
+ default=None,
+ action='store_true',
+ help='print the config in json format (cfg file applied)')
+
parser.add_argument('--show-config', dest='show_config',
default=None,
action='store_true',
- help='print the running config in json format')
+ help='print the running config in json format (final)')
parser.add_argument('-ss', '--show-summary', dest='summary',
action='store',
@@ -430,8 +527,109 @@ def _parse_opts_from_cli():
parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
action='store',
- metavar='<vlan>',
- help='Port to port or port to switch to port L2 loopback with VLAN id')
+ metavar='<vlan(s)|no-tag|true|false>',
+ help='Port to port or port to switch to port L2 loopback '
+ 'tagged with given VLAN id(s) or not (given \'no-tag\') '
+ '\'true\': use current vlans; \'false\': disable this mode.')
+
+ parser.add_argument('--i40e-mixed', dest='i40e_mixed',
+ action='store',
+ default=None,
+ metavar='<ignore,check,unbind>',
+ help='TRex behavior when dealing with a i40e network card driver'
+ ' [ https://trex-tgn.cisco.com/youtrack/issue/trex-528 ]')
+
+ parser.add_argument('--user-info', dest='user_info',
+ action='append',
+ metavar='<data>',
+ help='Custom data to be included as is '
+ 'in the json report config branch - '
+ ' example, pay attention! no space: '
+ '--user-info=\'{"status":"explore","description":'
+ '{"target":"lab","ok":true,"version":2020}}\' - '
+ 'this option may be repeated; given data will be merged.')
+
+ parser.add_argument('--vlan-tagging', dest='vlan_tagging',
+ type=bool_arg,
+ metavar='<boolean>',
+ action='store',
+ default=None,
+ help='Override the NFVbench \'vlan_tagging\' parameter')
+
+ parser.add_argument('--intf-speed', dest='intf_speed',
+ metavar='<speed>',
+ action='store',
+ default=None,
+ help='Override the NFVbench \'intf_speed\' '
+ 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
+
+ parser.add_argument('--cores', dest='cores',
+ type=int_arg,
+ metavar='<number>',
+ action='store',
+ default=None,
+ help='Override the T-Rex \'cores\' parameter')
+
+ parser.add_argument('--cache-size', dest='cache_size',
+ type=int_arg,
+ metavar='<size>',
+ action='store',
+ default=None,
+ help='Specify the FE cache size (default: 0, flow-count if < 0)')
+
+ parser.add_argument('--service-mode', dest='service_mode',
+ action='store_true',
+ default=None,
+ help='Enable T-Rex service mode (for debugging purpose)')
+
+ parser.add_argument('--no-e2e-check', dest='no_e2e_check',
+ action='store_true',
+ default=None,
+ help='Skip "end to end" connectivity check (on test purpose)')
+
+ parser.add_argument('--no-flow-stats', dest='no_flow_stats',
+ action='store_true',
+ default=None,
+ help='Disable additional flow stats (on high load traffic)')
+
+ parser.add_argument('--no-latency-stats', dest='no_latency_stats',
+ action='store_true',
+ default=None,
+ help='Disable flow stats for latency traffic')
+
+ parser.add_argument('--no-latency-streams', dest='no_latency_streams',
+ action='store_true',
+ default=None,
+ help='Disable latency measurements (no streams)')
+
+ parser.add_argument('--user-id', dest='user_id',
+ type=int_arg,
+ metavar='<uid>',
+ action='store',
+ default=None,
+ help='Change json/log files ownership with this user (int)')
+
+ parser.add_argument('--group-id', dest='group_id',
+ type=int_arg,
+ metavar='<gid>',
+ action='store',
+ default=None,
+ help='Change json/log files ownership with this group (int)')
+
+ parser.add_argument('--show-trex-log', dest='show_trex_log',
+ default=None,
+ action='store_true',
+ help='Show the current TRex local server log file contents'
+ ' => diagnostic/help in case of configuration problems')
+
+ parser.add_argument('--debug-mask', dest='debug_mask',
+ type=int_arg,
+ metavar='<mask>',
+ action='store',
+ default=None,
+ help='General purpose register (debugging flags), '
+ 'the hexadecimal notation (0x...) is accepted.'
+ 'Designed for development needs (default: 0).')
opts, unknown_opts = parser.parse_known_args()
return opts, unknown_opts
@@ -497,13 +695,20 @@ def main():
log.setup()
# load default config file
config, default_cfg = load_default_config()
+ # possibly override the default user_id & group_id values
+ if 'USER_ID' in os.environ:
+ config.user_id = int(os.environ['USER_ID'])
+ if 'GROUP_ID' in os.environ:
+ config.group_id = int(os.environ['GROUP_ID'])
+
# create factory for platform specific classes
try:
factory_module = importlib.import_module(config['factory_module'])
factory = getattr(factory_module, config['factory_class'])()
except AttributeError:
raise Exception("Requested factory module '{m}' or class '{c}' was not found."
- .format(m=config['factory_module'], c=config['factory_class']))
+ .format(m=config['factory_module'],
+ c=config['factory_class'])) from AttributeError
# create config plugin for this platform
config_plugin = factory.get_config_plugin_class()(config)
config = config_plugin.get_config()
@@ -512,26 +717,40 @@ def main():
log.set_level(debug=opts.debug)
if opts.version:
- print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
+ print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
sys.exit(0)
if opts.summary:
- with open(opts.summary) as json_data:
+ with open(opts.summary, encoding="utf-8") as json_data:
result = json.load(json_data)
if opts.user_label:
result['config']['user_label'] = opts.user_label
- print NFVBenchSummarizer(result, fluent_logger)
+ print((NFVBenchSummarizer(result, fluent_logger)))
sys.exit(0)
# show default config in text/yaml format
if opts.show_default_config:
- print default_cfg
+ print((default_cfg.decode("utf-8")))
+ sys.exit(0)
+
+ # dump the contents of the trex log file
+ if opts.show_trex_log:
+ try:
+ with open('/tmp/trex.log', encoding="utf-8") as trex_log_file:
+ print(trex_log_file.read(), end="")
+ except FileNotFoundError:
+ print("No TRex log file found!")
sys.exit(0)
+ # mask info logging in case of further config dump
+ if opts.show_config or opts.show_pre_config:
+ LOG.setLevel(log.logging.WARNING)
+
config.name = ''
if opts.config:
# do not check extra_specs in flavor as it can contain any key/value pairs
- whitelist_keys = ['extra_specs']
+ # the same principle applies also to the optional user_info open property
+ whitelist_keys = ['extra_specs', 'user_info']
# override default config options with start config at path parsed from CLI
# check if it is an inline yaml/json config or a file name
if os.path.isfile(opts.config):
@@ -542,6 +761,11 @@ def main():
LOG.info('Loading configuration string: %s', opts.config)
config = config_loads(opts.config, config, whitelist_keys)
+ # show current config in json format (before CLI overriding)
+ if opts.show_pre_config:
+ print((json.dumps(config, sort_keys=True, indent=4)))
+ sys.exit(0)
+
# setup the fluent logger as soon as possible right after the config plugin is called,
# if there is any logging or result tag is set then initialize the fluent logger
for fluentd in config.fluentd:
@@ -553,41 +777,124 @@ def main():
# traffic profile override options
override_custom_traffic(config, opts.frame_sizes, opts.unidir)
- # copy over cli options that are used in config
+ # Copy over some of the cli options that are used in config.
+ # This explicit copy is sometimes necessary
+ # because some early evaluation depends on them
+ # and cannot wait for _update_config() coming further.
+ # It is good practice then to set them to None (<=> done)
+ # and even required if a specific conversion is performed here
+ # that would be corrupted by a default update (simple copy).
+ # On the other hand, some excessive assignments have been removed
+ # from here, since the _update_config() procedure does them well.
+
config.generator_profile = opts.generator_profile
- if opts.sriov:
+ if opts.sriov is not None:
config.sriov = True
- if opts.log_file:
+ opts.sriov = None
+ if opts.log_file is not None:
config.log_file = opts.log_file
- if opts.service_chain:
+ opts.log_file = None
+ if opts.user_id is not None:
+ config.user_id = opts.user_id
+ opts.user_id = None
+ if opts.group_id is not None:
+ config.group_id = opts.group_id
+ opts.group_id = None
+ if opts.service_chain is not None:
config.service_chain = opts.service_chain
- if opts.service_chain_count:
- config.service_chain_count = opts.service_chain_count
- if opts.no_vswitch_access:
- config.no_vswitch_access = opts.no_vswitch_access
- if opts.hypervisor:
+ opts.service_chain = None
+ if opts.hypervisor is not None:
# can be any of 'comp1', 'nova:', 'nova:comp1'
config.compute_nodes = opts.hypervisor
- if opts.vxlan:
- config.vxlan = True
- if opts.restart:
- config.restart = True
- # port to port loopback (direct or through switch)
- if opts.l2_loopback:
- config.l2_loopback = True
- if config.service_chain != ChainType.EXT:
- LOG.info('Changing service chain type to EXT')
- config.service_chain = ChainType.EXT
- if not config.no_arp:
- LOG.info('Disabling ARP')
- config.no_arp = True
- config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
- LOG.info('Running L2 loopback: using EXT chain/no ARP')
+ opts.hypervisor = None
+ if opts.debug_mask is not None:
+ config.debug_mask = opts.debug_mask
+ opts.debug_mask = None
+
+ # convert 'user_info' opt from json string to dictionnary
+ # and merge the result with the current config dictionnary
+ if opts.user_info is not None:
+ for user_info_json in opts.user_info:
+ user_info_dict = json.loads(user_info_json)
+ if config.user_info:
+ config.user_info = config.user_info + user_info_dict
+ else:
+ config.user_info = user_info_dict
+ opts.user_info = None
- if opts.use_sriov_middle_net:
- if (not config.sriov) or (config.service_chain != ChainType.PVVP):
- raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
- config.use_sriov_middle_net = True
+ # port to port loopback (direct or through switch)
+ # we accept the following syntaxes for the CLI argument
+ # 'false' : mode not enabled
+ # 'true' : mode enabled with currently defined vlan IDs
+ # 'no-tag' : mode enabled with no vlan tagging
+ # <vlan IDs>: mode enabled using the given (pair of) vlan ID lists
+ # - If present, a '_' char will separate left an right ports lists
+ # e.g. 'a_x' => vlans: [[a],[x]]
+ # 'a,b,c_x,y,z' => [[a,b,c],[x,y,z]]
+ # - Otherwise the given vlan ID list applies to both sides
+ # e.g. 'a' => vlans: [[a],[a]]
+ # 'a,b' => [[a,b],[a,b]]
+ # - Vlan lists size needs to be at least the actual SCC value
+ # - Unless overriden in CLI opts, config.service_chain_count
+ # is adjusted to the size of the VLAN ID lists given here.
+
+ if opts.l2_loopback is not None:
+ arg_pair = opts.l2_loopback.lower().split('_')
+ if arg_pair[0] == 'false':
+ config.l2_loopback = False
+ else:
+ config.l2_loopback = True
+ if config.service_chain != ChainType.EXT:
+ LOG.info('Changing service chain type to EXT')
+ config.service_chain = ChainType.EXT
+ if not config.no_arp:
+ LOG.info('Disabling ARP')
+ config.no_arp = True
+ if arg_pair[0] == 'true':
+ pass
+ else:
+ # here explicit (not)tagging is not CLI overridable
+ opts.vlan_tagging = None
+ if arg_pair[0] == 'no-tag':
+ config.vlan_tagging = False
+ else:
+ config.vlan_tagging = True
+ if len(arg_pair) == 1 or not arg_pair[1]:
+ arg_pair = [arg_pair[0], arg_pair[0]]
+ vlans = [[], []]
+
+ def append_vlan(port, vlan_id):
+ # a vlan tag value must be in [0..4095]
+ if vlan_id not in range(0, 4096):
+ raise ValueError
+ vlans[port].append(vlan_id)
+ try:
+ for port in [0, 1]:
+ vlan_ids = arg_pair[port].split(',')
+ for vlan_id in vlan_ids:
+ append_vlan(port, int(vlan_id))
+ if len(vlans[0]) != len(vlans[1]):
+ raise ValueError
+ except ValueError:
+ # at least one invalid tag => no tagging
+ config.vlan_tagging = False
+ if config.vlan_tagging:
+ config.vlans = vlans
+ # force service chain count if not CLI overriden
+ if opts.service_chain_count is None:
+ config.service_chain_count = len(vlans[0])
+ opts.l2_loopback = None
+
+ if config.i40e_mixed is None:
+ config.i40e_mixed = 'ignore'
+ if config.use_sriov_middle_net is None:
+ config.use_sriov_middle_net = False
+ if opts.use_sriov_middle_net is not None:
+ config.use_sriov_middle_net = opts.use_sriov_middle_net
+ opts.use_sriov_middle_net = None
+ if (config.use_sriov_middle_net and (
+ (not config.sriov) or (config.service_chain != ChainType.PVVP))):
+ raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
if config.sriov and config.service_chain != ChainType.EXT:
# if sriov is requested (does not apply to ext chains)
@@ -597,11 +904,6 @@ def main():
if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
check_physnet("middle", config.internal_networks.middle)
- # show running config in json format
- if opts.show_config:
- print json.dumps(config, sort_keys=True, indent=4)
- sys.exit(0)
-
# update the config in the config plugin as it might have changed
# in a copy of the dict (config plugin still holds the original dict)
config_plugin.set_config(config)
@@ -612,6 +914,13 @@ def main():
# add file log if requested
if config.log_file:
log.add_file_logger(config.log_file)
+ # possibly change file ownership
+ uid = config.user_id
+ gid = config.group_id
+ if gid is None:
+ gid = uid
+ if uid is not None:
+ os.chown(config.log_file, uid, gid)
openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
else None
@@ -628,6 +937,7 @@ def main():
server.run(host=opts.host, port=port)
# server.run() should never return
else:
+ dry_run = opts.show_config
with utils.RunLock():
run_summary_required = True
if unknown_opts:
@@ -636,10 +946,10 @@ def main():
raise Exception(err_msg)
# remove unfilled values
- opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
+ opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
# get CLI args
params = ' '.join(str(e) for e in sys.argv[1:])
- result = nfvbench_instance.run(opts, params)
+ result = nfvbench_instance.run(opts, params, dry_run=dry_run)
if 'error_message' in result:
raise Exception(result['error_message'])
@@ -652,7 +962,7 @@ def main():
'status': NFVBench.STATUS_ERROR,
'error_message': traceback.format_exc()
})
- print str(exc)
+ print((str(exc)))
finally:
if fluent_logger:
# only send a summary record if there was an actual nfvbench run or