summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rw-r--r--.gitignore13
-rw-r--r--Dockerfile35
-rw-r--r--INFO11
-rw-r--r--README.rst17
-rw-r--r--cleanup/__init__.py0
-rw-r--r--cleanup/nfvbench_cleanup.py594
-rw-r--r--client/__init__.py0
-rw-r--r--client/client.py150
-rw-r--r--client/nfvbench_client.py88
-rw-r--r--client/requirements.txt7
-rw-r--r--conftest.py0
-rwxr-xr-xdocker/cleanup_generators.py78
-rw-r--r--docs/Makefile225
-rw-r--r--docs/development/design/design.rst10
-rw-r--r--docs/development/design/index.rst10
-rw-r--r--docs/development/index.rst10
-rw-r--r--docs/development/overview/index.rst13
-rw-r--r--docs/development/overview/overview.rst13
-rw-r--r--docs/index.rst11
-rw-r--r--docs/release/release-notes/index.rst11
-rw-r--r--docs/release/release-notes/release-notes.rst59
-rw-r--r--docs/testing/developer/devguide/index.rst0
-rw-r--r--docs/testing/index.rst11
-rw-r--r--docs/testing/user/configguide/configguide.rst0
-rw-r--r--docs/testing/user/configguide/index.rst9
-rw-r--r--docs/testing/user/userguide/_static/custom.css4
-rw-r--r--docs/testing/user/userguide/_templates/layout.html5
-rw-r--r--docs/testing/user/userguide/advanced.rst318
-rw-r--r--docs/testing/user/userguide/conf.py344
-rw-r--r--docs/testing/user/userguide/examples.rst9
-rw-r--r--docs/testing/user/userguide/faq.rst28
-rw-r--r--docs/testing/user/userguide/hw_requirements.rst79
-rw-r--r--docs/testing/user/userguide/images/extchain-config.svg219
-rw-r--r--docs/testing/user/userguide/images/nfvbench-npvp.svg107
-rw-r--r--docs/testing/user/userguide/images/nfvbench-pvp.svg94
-rw-r--r--docs/testing/user/userguide/images/nfvbench-pvvp-inter.svg132
-rw-r--r--docs/testing/user/userguide/images/nfvbench-pvvp-intra.svg114
-rw-r--r--docs/testing/user/userguide/images/nfvbench-spirent-setup.svg170
-rw-r--r--docs/testing/user/userguide/images/nfvbench-trex-setup.svg170
-rw-r--r--docs/testing/user/userguide/index.rst30
-rw-r--r--docs/testing/user/userguide/installation.rst14
-rw-r--r--docs/testing/user/userguide/quickstart_docker.rst224
-rw-r--r--docs/testing/user/userguide/readme.rst163
-rw-r--r--docs/testing/user/userguide/server.rst445
-rw-r--r--nfvbench/__init__.py18
-rw-r--r--nfvbench/cfg.default.yaml337
-rw-r--r--nfvbench/chain_clients.py564
-rw-r--r--nfvbench/chain_managers.py231
-rw-r--r--nfvbench/chain_runner.py82
-rw-r--r--nfvbench/chain_workers.py53
-rw-r--r--nfvbench/compute.py483
-rw-r--r--nfvbench/config.py56
-rw-r--r--nfvbench/config_plugin.py87
-rw-r--r--nfvbench/connection.py725
-rw-r--r--nfvbench/credentials.py166
-rw-r--r--nfvbench/factory.py70
-rw-r--r--nfvbench/log.py40
-rw-r--r--nfvbench/network.py62
-rw-r--r--nfvbench/nfvbench.py491
-rw-r--r--nfvbench/nfvbenchd.py251
-rw-r--r--nfvbench/nfvbenchvm/nfvbenchvm.conf9
-rw-r--r--nfvbench/packet_analyzer.py64
-rw-r--r--nfvbench/service_chain.py138
-rw-r--r--nfvbench/specs.py93
-rw-r--r--nfvbench/stats_collector.py145
-rw-r--r--nfvbench/summarizer.py402
-rw-r--r--nfvbench/tor_client.py52
-rw-r--r--nfvbench/traffic_client.py790
-rw-r--r--nfvbench/traffic_gen/__init__.py0
-rw-r--r--nfvbench/traffic_gen/dummy.py95
-rw-r--r--nfvbench/traffic_gen/traffic_base.py89
-rw-r--r--nfvbench/traffic_gen/traffic_utils.py160
-rw-r--r--nfvbench/traffic_gen/trex.py456
-rw-r--r--nfvbench/traffic_server.py64
-rw-r--r--nfvbench/utils.py170
-rw-r--r--nfvbenchvm/README.rst84
-rw-r--r--requirements-dev.txt8
-rw-r--r--requirements.txt26
-rw-r--r--setup.cfg46
-rw-r--r--setup.py42
-rw-r--r--test-requirements.txt17
-rw-r--r--test/test_nfvbench.py640
-rw-r--r--tox.ini43
84 files changed, 11395 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..642121f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+requirements-dev.txt
+.gitignore \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89615b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+.DS_store
+*.pyc
+.idea
+.tox
+.cache
+.eggs
+venv
+nfvbench.egg-info
+docs/_build
+nfvbenchvm/dib/
+*.qcow2
+docs/conf.py
+docs/_static \ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2c8a361
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,35 @@
+# docker file for creating a container that has nfvbench installed and ready to use
+FROM ubuntu:16.04
+
+COPY . /nfvbench
+
+ENV TREX_VER "v2.27"
+
+RUN apt-get update && apt-get install -y \
+ git \
+ kmod \
+ pciutils \
+ python \
+ python-pip \
+ vim \
+ wget \
+ net-tools \
+ && mkdir -p /opt/trex \
+ && wget --no-cache https://trex-tgn.cisco.com/trex/release/$TREX_VER.tar.gz \
+ && tar xzf $TREX_VER.tar.gz -C /opt/trex \
+ && rm -f /$TREX_VER.tar.gz \
+ && rm -f /opt/trex/$TREX_VER/trex_client_$TREX_VER.tar.gz \
+ && cp -a /opt/trex/$TREX_VER/automation/trex_control_plane/stl/trex_stl_lib /usr/local/lib/python2.7/dist-packages/ \
+ && rm -rf /opt/trex/$TREX_VER/automation/trex_control_plane/stl/trex_stl_lib \
+ && sed -i -e "s/2048 /512 /" -e "s/2048\"/512\"/" /opt/trex/$TREX_VER/trex-cfg \
+ && pip install -U pip pbr \
+ && pip install -U setuptools \
+ && cd /nfvbench && pip install -e . \
+ && python ./docker/cleanup_generators.py \
+ && rm -rf /nfvbench/.git \
+ && apt-get remove -y wget git \
+ && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+ENV TREX_STL_EXT_PATH "/opt/trex/$TREX_VER/external_libs"
+
+CMD ["tail", "-f", "/dev/null"]
diff --git a/INFO b/INFO
index 94694a0..9d89699 100644
--- a/INFO
+++ b/INFO
@@ -18,3 +18,14 @@ Yichen Wang (yicwang@cisco.com)
Al Morton (acmorton@att.com)
Link to TSC approval of the project:
+
+Acknowledgements
+The development of NFVbench started in Summer 2016 at Cisco by this small team of dedicated people
+before being open sourced in Summer 2017 to OPNFV following more than 500 commits:
+ Jan Balaz (aka Johnny)
+ Stefano Chiesa Suryanto
+ Yichen Wang
+ Alec Hothan
+
+
+
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..567ae7d
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,17 @@
+NFVbench: A Network Performance Benchmarking Tool for NFVi Full Stacks
+**********************************************************************
+
+The NFVbench tool provides an automated way to measure the network performance for the most common data plane packet flows on any OpenStack based NFVi system viewed as a black box (NFVi Full Stack).
+An NFVi full stack exposes the following interfaces:
+- an OpenStack API
+- an interface to send and receive packets on the data plane (typically through top of rack switches)
+
+The NFVi full stack does not have to be supported by the OPNFV ecosystem and can be any functional OpenStack system that provides the aboce interfaces. NFVbench can be installed standalone (in the form of a single Docker container) and is fully functional without the need to install any other OPNFV framework.
+
+It is designed to be easy to install and easy to use by non experts (no need to be an expert in traffic generators and data plane performance testing).
+
+
+
+
+
+
diff --git a/cleanup/__init__.py b/cleanup/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cleanup/__init__.py
diff --git a/cleanup/nfvbench_cleanup.py b/cleanup/nfvbench_cleanup.py
new file mode 100644
index 0000000..1520647
--- /dev/null
+++ b/cleanup/nfvbench_cleanup.py
@@ -0,0 +1,594 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+###############################################################################
+# #
+# This is a helper script which will delete all resources created by #
+# NFVbench. #
+# #
+# Normally, NFVbench will clean up automatically when it is done. However, #
+# sometimes errors or timeouts happen during the resource creation stage, #
+# which will cause NFVbench out of sync with the real environment. If that #
+# happens, a force cleanup may be needed. #
+# #
+# It is safe to use the script with the resource list generated by #
+# NFVbench, usage: #
+# $ python nfvbench_cleanup.py -r /path/to/openrc #
+# #
+# Note: If running under single-tenant or tenant/user reusing mode, you have #
+# to cleanup the server resources first, then client resources. #
+# #
+# When there is no resource list provided, the script will simply grep the #
+# resource name with "nfvbench" and delete them. If running on a production #
+# network, please double and triple check all resources names are *NOT* #
+# starting with "nfvbench", otherwise they will be deleted by the script. #
+# #
+###############################################################################
+
+# ======================================================
+# WARNING
+# ======================================================
+# IMPORTANT FOR RUNNING NFVbench ON PRODUCTION CLOUDS
+#
+# DOUBLE CHECK THE NAMES OF ALL RESOURCES THAT DO NOT
+# BELONG TO NFVbench ARE *NOT* STARTING WITH "nfvbench".
+# ======================================================
+
+from abc import ABCMeta
+from abc import abstractmethod
+import argparse
+import re
+import sys
+import time
+import traceback
+
+# openstack python clients
+import cinderclient
+from keystoneclient import client as keystoneclient
+import neutronclient
+from novaclient.exceptions import NotFound
+from tabulate import tabulate
+
+from nfvbench import credentials
+
+resource_name_re = None
+
+def prompt_to_run():
+ print "Warning: You didn't specify a resource list file as the input. "\
+ "The script will delete all resources shown above."
+ answer = raw_input("Are you sure? (y/n) ")
+ if answer.lower() != 'y':
+ sys.exit(0)
+
+def fetch_resources(fetcher, options=None):
+ try:
+ if options:
+ res_list = fetcher(search_opts=options)
+ else:
+ res_list = fetcher()
+ except Exception as e:
+ res_list = []
+ traceback.print_exc()
+ print "Warning exception while listing resources:" + str(e)
+ resources = {}
+ for res in res_list:
+ # some objects provide direct access some
+ # require access by key
+ try:
+ resid = res.id
+ resname = res.name
+ except AttributeError:
+ resid = res['id']
+ resname = res['name']
+ if resname and resource_name_re.match(resname):
+ resources[resid] = resname
+ return resources
+
+class AbstractCleaner(object):
+ __metaclass__ = ABCMeta
+
+ def __init__(self, res_category, res_desc, resources, dryrun):
+ self.dryrun = dryrun
+ self.category = res_category
+ self.resources = {}
+ if not resources:
+ print 'Discovering %s resources...' % (res_category)
+ for rtype, fetch_args in res_desc.iteritems():
+ if resources:
+ if rtype in resources:
+ self.resources[rtype] = resources[rtype]
+ else:
+ self.resources[rtype] = fetch_resources(*fetch_args)
+
+ def report_deletion(self, rtype, name):
+ if self.dryrun:
+ print ' + ' + rtype + ' ' + name + ' should be deleted (but is not deleted: dry run)'
+ else:
+ print ' + ' + rtype + ' ' + name + ' is successfully deleted'
+
+ def report_not_found(self, rtype, name):
+ print ' ? ' + rtype + ' ' + name + ' not found (already deleted?)'
+
+ def report_error(self, rtype, name, reason):
+ print ' - ' + rtype + ' ' + name + ' ERROR:' + reason
+
+ def get_resource_list(self):
+ result = []
+ for rtype, rdict in self.resources.iteritems():
+ for resid, resname in rdict.iteritems():
+ result.append([rtype, resname, resid])
+ return result
+
+ @abstractmethod
+ def clean(self):
+ pass
+
+class StorageCleaner(AbstractCleaner):
+ def __init__(self, sess, resources, dryrun):
+ from cinderclient import client as cclient
+ from novaclient import client as nclient
+
+ self.nova = nclient.Client('2', endpoint_type='publicURL', session=sess)
+ self.cinder = cclient.Client('2', endpoint_type='publicURL', session=sess)
+
+ res_desc = {'volumes': [self.cinder.volumes.list, {"all_tenants": 1}]}
+ super(StorageCleaner, self).__init__('Storage', res_desc, resources, dryrun)
+
+ def clean(self):
+ print '*** STORAGE cleanup'
+ try:
+ volumes = []
+ detaching_volumes = []
+ for id, name in self.resources['volumes'].iteritems():
+ try:
+ vol = self.cinder.volumes.get(id)
+ if vol.attachments:
+ # detach the volume
+ try:
+ if not self.dryrun:
+ ins_id = vol.attachments[0]['server_id']
+ self.nova.volumes.delete_server_volume(ins_id, id)
+ print ' . VOLUME ' + vol.name + ' detaching...'
+ else:
+ print ' . VOLUME ' + vol.name + ' to be detached...'
+ detaching_volumes.append(vol)
+ except NotFound:
+ print 'WARNING: Volume %s attached to an instance that no longer '\
+ 'exists (will require manual cleanup of the database)' % (id)
+ except Exception as e:
+ print str(e)
+ else:
+ # no attachments
+ volumes.append(vol)
+ except cinderclient.exceptions.NotFound:
+ self.report_not_found('VOLUME', name)
+
+ # check that the volumes are no longer attached
+ if detaching_volumes:
+ if not self.dryrun:
+ print ' . Waiting for %d volumes to be fully detached...' % \
+ (len(detaching_volumes))
+ retry_count = 5 + len(detaching_volumes)
+ while True:
+ retry_count -= 1
+ for vol in list(detaching_volumes):
+ if not self.dryrun:
+ latest_vol = self.cinder.volumes.get(detaching_volumes[0].id)
+ if self.dryrun or not latest_vol.attachments:
+ if not self.dryrun:
+ print ' + VOLUME ' + vol.name + ' detach complete'
+ detaching_volumes.remove(vol)
+ volumes.append(vol)
+ if detaching_volumes and not self.dryrun:
+ if retry_count:
+ print ' . VOLUME %d left to be detached, retries left=%d...' % \
+ (len(detaching_volumes), retry_count)
+ time.sleep(2)
+ else:
+ print ' - VOLUME detach timeout, %d volumes left:' % \
+ (len(detaching_volumes))
+ for vol in detaching_volumes:
+ print ' ', vol.name, vol.status, vol.id, vol.attachments
+ break
+ else:
+ break
+
+ # finally delete the volumes
+ for vol in volumes:
+ if not self.dryrun:
+ try:
+ vol.force_delete()
+ except cinderclient.exceptions.BadRequest as exc:
+ print str(exc)
+ self.report_deletion('VOLUME', vol.name)
+ except KeyError:
+ pass
+
+class ComputeCleaner(AbstractCleaner):
+ def __init__(self, sess, resources, dryrun):
+ from neutronclient.neutron import client as nclient
+ from novaclient import client as novaclient
+ self.neutron_client = nclient.Client('2.0', endpoint_type='publicURL', session=sess)
+ self.nova_client = novaclient.Client('2', endpoint_type='publicURL', session=sess)
+ res_desc = {
+ 'instances': [self.nova_client.servers.list, {"all_tenants": 1}],
+ 'flavors': [self.nova_client.flavors.list],
+ 'keypairs': [self.nova_client.keypairs.list]
+ }
+ super(ComputeCleaner, self).__init__('Compute', res_desc, resources, dryrun)
+
+ def clean(self):
+ print '*** COMPUTE cleanup'
+ try:
+ # Get a list of floating IPs
+ fip_lst = self.neutron_client.list_floatingips()['floatingips']
+ deleting_instances = self.resources['instances']
+ for id, name in self.resources['instances'].iteritems():
+ try:
+ if self.nova_client.servers.get(id).addresses.values():
+ ins_addr = self.nova_client.servers.get(id).addresses.values()[0]
+ fips = [x['addr'] for x in ins_addr if x['OS-EXT-IPS:type'] == 'floating']
+ else:
+ fips = []
+ if self.dryrun:
+ self.nova_client.servers.get(id)
+ for fip in fips:
+ self.report_deletion('FLOATING IP', fip)
+ self.report_deletion('INSTANCE', name)
+ else:
+ for fip in fips:
+ fip_id = [x['id'] for x in fip_lst if x['floating_ip_address'] == fip]
+ self.neutron_client.delete_floatingip(fip_id[0])
+ self.report_deletion('FLOATING IP', fip)
+ self.nova_client.servers.delete(id)
+ except NotFound:
+ deleting_instances.remove(id)
+ self.report_not_found('INSTANCE', name)
+
+ if not self.dryrun and len(deleting_instances):
+ print ' . Waiting for %d instances to be fully deleted...' % \
+ (len(deleting_instances))
+ retry_count = 5 + len(deleting_instances)
+ while True:
+ retry_count -= 1
+ for ins_id in deleting_instances.keys():
+ try:
+ self.nova_client.servers.get(ins_id)
+ except NotFound:
+ self.report_deletion('INSTANCE', deleting_instances[ins_id])
+ deleting_instances.pop(ins_id)
+
+ if not len(deleting_instances):
+ break
+
+ if retry_count:
+ print ' . INSTANCE %d left to be deleted, retries left=%d...' % \
+ (len(deleting_instances), retry_count)
+ time.sleep(2)
+ else:
+ print ' - INSTANCE deletion timeout, %d instances left:' % \
+ (len(deleting_instances))
+ for ins_id in deleting_instances.keys():
+ try:
+ ins = self.nova_client.servers.get(ins_id)
+ print ' ', ins.name, ins.status, ins.id
+ except NotFound:
+ print(' ', deleting_instances[ins_id],
+ '(just deleted)', ins_id)
+ break
+ except KeyError:
+ pass
+
+ try:
+ for id, name in self.resources['flavors'].iteritems():
+ try:
+ flavor = self.nova_client.flavors.find(name=name)
+ if not self.dryrun:
+ flavor.delete()
+ self.report_deletion('FLAVOR', name)
+ except NotFound:
+ self.report_not_found('FLAVOR', name)
+ except KeyError:
+ pass
+
+ try:
+ for id, name in self.resources['keypairs'].iteritems():
+ try:
+ if self.dryrun:
+ self.nova_client.keypairs.get(name)
+ else:
+ self.nova_client.keypairs.delete(name)
+ self.report_deletion('KEY PAIR', name)
+ except NotFound:
+ self.report_not_found('KEY PAIR', name)
+ except KeyError:
+ pass
+
+class NetworkCleaner(AbstractCleaner):
+
+ def __init__(self, sess, resources, dryrun):
+ from neutronclient.neutron import client as nclient
+ self.neutron = nclient.Client('2.0', endpoint_type='publicURL', session=sess)
+
+ # because the response has an extra level of indirection
+ # we need to extract it to present the list of network or router objects
+ def networks_fetcher():
+ return self.neutron.list_networks()['networks']
+
+ def routers_fetcher():
+ return self.neutron.list_routers()['routers']
+
+ def secgroup_fetcher():
+ return self.neutron.list_security_groups()['security_groups']
+
+ res_desc = {
+ 'sec_groups': [secgroup_fetcher],
+ 'networks': [networks_fetcher],
+ 'routers': [routers_fetcher]
+ }
+ super(NetworkCleaner, self).__init__('Network', res_desc, resources, dryrun)
+
+ def remove_router_interface(self, router_id, port):
+ """
+ Remove the network interface from router
+ """
+ body = {
+ # 'port_id': port['id']
+ 'subnet_id': port['fixed_ips'][0]['subnet_id']
+ }
+ try:
+ self.neutron.remove_interface_router(router_id, body)
+ self.report_deletion('Router Interface', port['fixed_ips'][0]['ip_address'])
+ except neutronclient.common.exceptions.NotFound:
+ pass
+
+ def remove_network_ports(self, net):
+ """
+ Remove ports belonging to network
+ """
+ for port in filter(lambda p: p['network_id'] == net, self.neutron.list_ports()['ports']):
+ try:
+ self.neutron.delete_port(port['id'])
+ self.report_deletion('Network port', port['id'])
+ except neutronclient.common.exceptions.NotFound:
+ pass
+
+ def clean(self):
+ print '*** NETWORK cleanup'
+
+ try:
+ for id, name in self.resources['sec_groups'].iteritems():
+ try:
+ if self.dryrun:
+ self.neutron.show_security_group(id)
+ else:
+ self.neutron.delete_security_group(id)
+ self.report_deletion('SECURITY GROUP', name)
+ except NotFound:
+ self.report_not_found('SECURITY GROUP', name)
+ except KeyError:
+ pass
+
+ try:
+ for id, name in self.resources['floating_ips'].iteritems():
+ try:
+ if self.dryrun:
+ self.neutron.show_floatingip(id)
+ else:
+ self.neutron.delete_floatingip(id)
+ self.report_deletion('FLOATING IP', name)
+ except neutronclient.common.exceptions.NotFound:
+ self.report_not_found('FLOATING IP', name)
+ except KeyError:
+ pass
+
+ try:
+ for id, name in self.resources['routers'].iteritems():
+ try:
+ if self.dryrun:
+ self.neutron.show_router(id)
+ self.report_deletion('Router Gateway', name)
+ port_list = self.neutron.list_ports(id)['ports']
+ for port in port_list:
+ if 'fixed_ips' in port:
+ self.report_deletion('Router Interface',
+ port['fixed_ips'][0]['ip_address'])
+ else:
+ self.neutron.remove_gateway_router(id)
+ self.report_deletion('Router Gateway', name)
+ # need to delete each interface before deleting the router
+ port_list = self.neutron.list_ports(id)['ports']
+ for port in port_list:
+ self.remove_router_interface(id, port)
+ self.neutron.delete_router(id)
+ self.report_deletion('ROUTER', name)
+ except neutronclient.common.exceptions.NotFound:
+ self.report_not_found('ROUTER', name)
+ except neutronclient.common.exceptions.Conflict as exc:
+ self.report_error('ROUTER', name, str(exc))
+ except KeyError:
+ pass
+ try:
+ for id, name in self.resources['networks'].iteritems():
+ try:
+ if self.dryrun:
+ self.neutron.show_network(id)
+ else:
+ self.remove_network_ports(id)
+ self.neutron.delete_network(id)
+ self.report_deletion('NETWORK', name)
+ except neutronclient.common.exceptions.NetworkNotFoundClient:
+ self.report_not_found('NETWORK', name)
+ except neutronclient.common.exceptions.NetworkInUseClient as exc:
+ self.report_error('NETWORK', name, str(exc))
+ except KeyError:
+ pass
+
+class KeystoneCleaner(AbstractCleaner):
+
+ def __init__(self, sess, resources, dryrun):
+ self.keystone = keystoneclient.Client(endpoint_type='publicURL', session=sess)
+ self.tenant_api = self.keystone.tenants \
+ if self.keystone.version == 'v2.0' else self.keystone.projects
+ res_desc = {
+ 'users': [self.keystone.users.list],
+ 'tenants': [self.tenant_api.list]
+ }
+ super(KeystoneCleaner, self).__init__('Keystone', res_desc, resources, dryrun)
+
+ def clean(self):
+ print '*** KEYSTONE cleanup'
+ try:
+ for id, name in self.resources['users'].iteritems():
+ try:
+ if self.dryrun:
+ self.keystone.users.get(id)
+ else:
+ self.keystone.users.delete(id)
+ self.report_deletion('USER', name)
+ except keystoneclient.auth.exceptions.http.NotFound:
+ self.report_not_found('USER', name)
+ except KeyError:
+ pass
+
+ try:
+ for id, name in self.resources['tenants'].iteritems():
+ try:
+ if self.dryrun:
+ self.tenant_api.get(id)
+ else:
+ self.tenant_api.delete(id)
+ self.report_deletion('TENANT', name)
+ except keystoneclient.auth.exceptions.http.NotFound:
+ self.report_not_found('TENANT', name)
+ except KeyError:
+ pass
+
+class Cleaners(object):
+
+ def __init__(self, creds_obj, resources, dryrun):
+ self.cleaners = []
+ sess = creds_obj.get_session()
+ for cleaner_type in [StorageCleaner, ComputeCleaner, NetworkCleaner, KeystoneCleaner]:
+ self.cleaners.append(cleaner_type(sess, resources, dryrun))
+
+ def show_resources(self):
+ table = [["Type", "Name", "UUID"]]
+ for cleaner in self.cleaners:
+ table.extend(cleaner.get_resource_list())
+ count = len(table) - 1
+ print
+ if count:
+ print 'SELECTED RESOURCES:'
+ print tabulate(table, headers="firstrow", tablefmt="psql")
+ else:
+ print 'There are no resources to delete.'
+ print
+ return count
+
+ def clean(self):
+ for cleaner in self.cleaners:
+ cleaner.clean()
+
+# A dictionary of resources to cleanup
+# First level keys are:
+# flavors, keypairs, users, routers, floating_ips, instances, volumes, sec_groups, tenants, networks
+# second level keys are the resource IDs
+# values are the resource name (e.g. 'nfvbench-net0')
+def get_resources_from_cleanup_log(logfile):
+ '''Load triplets separated by '|' into a 2 level dictionary
+ '''
+ resources = {}
+ with open(logfile) as ff:
+ content = ff.readlines()
+ for line in content:
+ tokens = line.strip().split('|')
+ restype = tokens[0]
+ resname = tokens[1]
+ resid = tokens[2]
+ if not resid:
+ # normally only the keypairs have no ID
+ if restype != "keypairs":
+ print 'Error: resource type %s has no ID - ignored!!!' % (restype)
+ else:
+ resid = '0'
+ if restype not in resources:
+ resources[restype] = {}
+ tres = resources[restype]
+ tres[resid] = resname
+ return resources
+
+
+def main():
+ parser = argparse.ArgumentParser(description='NFVbench Force Cleanup')
+
+ parser.add_argument('-r', '--rc', dest='rc',
+ action='store', required=False,
+ help='openrc file',
+ metavar='<file>')
+ parser.add_argument('-f', '--file', dest='file',
+ action='store', required=False,
+ help='get resources to delete from cleanup log file '
+ '(default:discover from OpenStack)',
+ metavar='<file>')
+ parser.add_argument('-d', '--dryrun', dest='dryrun',
+ action='store_true',
+ default=False,
+ help='check resources only - do not delete anything')
+ parser.add_argument('--filter', dest='filter',
+ action='store', required=False,
+ help='resource name regular expression filter (default:"nfvbench")'
+ ' - OpenStack discovery only',
+ metavar='<any-python-regex>')
+ opts = parser.parse_args()
+
+ cred = credentials.Credentials(openrc_file=opts.rc)
+
+ if opts.file:
+ resources = get_resources_from_cleanup_log(opts.file)
+ else:
+ # None means try to find the resources from openstack directly by name
+ resources = None
+ global resource_name_re
+ if opts.filter:
+ try:
+ resource_name_re = re.compile(opts.filter)
+ except Exception as exc:
+ print 'Provided filter is not a valid python regular expression: ' + opts.filter
+ print str(exc)
+ sys.exit(1)
+ else:
+ resource_name_re = re.compile('nfvbench')
+
+ cleaners = Cleaners(cred, resources, opts.dryrun)
+
+ if opts.dryrun:
+ print
+ print('!!! DRY RUN - RESOURCES WILL BE CHECKED BUT WILL NOT BE DELETED !!!')
+ print
+
+ # Display resources to be deleted
+ count = cleaners.show_resources()
+ if not count:
+ sys.exit(0)
+
+ if not opts.file and not opts.dryrun:
+ prompt_to_run()
+
+ cleaners.clean()
+
+if __name__ == '__main__':
+ main()
diff --git a/client/__init__.py b/client/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/__init__.py
diff --git a/client/client.py b/client/client.py
new file mode 100644
index 0000000..5cbc733
--- /dev/null
+++ b/client/client.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import requests
+import time
+
+from socketIO_client import SocketIO
+
+
+class TimeOutException(Exception):
+ pass
+
+
+class NfvbenchException(Exception):
+ pass
+
+
+class NfvbenchClient(object):
+ """Python client class to control a nfvbench server
+
+ The nfvbench server must run in background using the --server option.
+ Since HTML pages are not required, the path to pass to --server can be
+ any directory on the host.
+ """
+ def __init__(self, nfvbench_url, use_socketio):
+ """Client class to send requests to the nfvbench server
+
+ Args:
+ nfvbench_url: the URL of the nfvbench server (e.g. 'http://127.0.0.1:7555')
+ """
+ self.url = nfvbench_url
+ self.use_socketio = use_socketio
+
+ def socketio_send(self, send_event, receive_event, config, timeout):
+ class Exec(object):
+ socketIO = None
+ socketio_result = None
+
+ def close_socketio(result):
+ Exec.socketio_result = result
+ Exec.socketIO.disconnect()
+
+ def on_response(*args):
+ close_socketio(args[0])
+
+ def on_error(*args):
+ raise NfvbenchException(args[0])
+
+ Exec.socketIO = SocketIO(self.url)
+ Exec.socketIO.on(receive_event, on_response)
+ Exec.socketIO.on('error', on_error)
+ Exec.socketIO.emit(send_event, config)
+ Exec.socketIO.wait(seconds=timeout)
+
+ if timeout and not Exec.socketio_result:
+ raise TimeOutException()
+ return Exec.socketio_result
+
+ def http_get(self, command, config):
+ url = self.url + '/' + command
+ res = requests.get(url, json=config)
+ if res.ok:
+ return res.json()
+ res.raise_for_status()
+
+ def http_post(self, command, config):
+ url = self.url + '/' + command
+ res = requests.post(url, json=config)
+ if res.ok:
+ return res.json()
+ res.raise_for_status()
+
+ def echo_config(self, config, timeout=100):
+ """Send an echo event to the nfvbench server with some dummy config and expect the
+ config to be sent back right away.
+
+ Args:
+ config: some dummy configuration - must be a valid dict
+ timeout: how long to wait in seconds or 0 to return immediately,
+ defaults to 100 seconds
+
+ Returns:
+ The config as passed as a dict or None if timeout passed is 0
+
+ Raises:
+ NfvbenchException: the execution of the passed configuration failed,
+ the body of the exception
+ containes the description of the failure.
+ TimeOutException: the request timed out (and might still being executed
+ by the server)
+ """
+ if self.use_socketio:
+ return self.socketio_send('echo', 'echo', config, timeout)
+ return self.http_get('echo', config)
+
+ def run_config(self, config, timeout=300, poll_interval=5):
+ """Request an nfvbench configuration to be executed by the nfvbench server.
+
+ This function will block the caller until the request completes or the request times out.
+ It can return immediately if timeout is set to 0.
+ Note that running a configuration may take a while depending on the amount of work
+ requested - so set the timeout value to an appropriate value.
+
+ Args:
+ config: the nfvbench configuration to execute - must be a valid dict with
+ valid nfvbench attributes
+ timeout: how long to wait in seconds or 0 to return immediately,
+ defaults to 300 seconds
+ poll_interval: seconds between polling (http only) - defaults to every 5 seconds
+
+ Returns:
+ The result of the nfvbench execution
+ or None if timeout passed is 0
+ The function will return as soon as the request is completed or when the
+ timeout occurs (whichever is first).
+
+ Raises:
+ NfvbenchException: the execution of the passed configuration failed, the body of
+ the exception contains the description of the failure.
+ TimeOutException: the request timed out but will still be executed by the server.
+ """
+ if self.use_socketio:
+ return self.socketio_send('start_run', 'run_end', config, timeout)
+ res = self.http_post('start_run', config)
+ if res['status'] != 'PENDING':
+ raise NfvbenchException(res['error_message'])
+
+ # poll until request completes
+ elapsed = 0
+ while True:
+ time.sleep(poll_interval)
+ result = self.http_get('status', config)
+ if result['status'] != 'PENDING':
+ return result
+ elapsed += poll_interval
+ if elapsed >= timeout:
+ raise TimeOutException()
diff --git a/client/nfvbench_client.py b/client/nfvbench_client.py
new file mode 100644
index 0000000..3973b9c
--- /dev/null
+++ b/client/nfvbench_client.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+#
+# This is an example of python application controling a nfvbench server
+# using the nfvbench client API.
+# The nfvbench server must run in background using the --server option.
+# Since HTML pages are not required, the path to pass to --server can be any directory on the host.
+#
+import argparse
+import json
+import sys
+
+from client import NfvbenchClient
+
+
+#
+# At the CLI, the user can either:
+# - pass an nfvbench configuration as a string (-c <config>)
+# - pass an nfvbench configuration as a file name containing the
+# configuration (-f <config_file_path>)
+# - or pass a test config (-e <config>) that will be echoed back by the server as is
+#
+def main():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-f', '--file', dest='file',
+ action='store',
+ help='NFVbench config file to execute (json format)',
+ metavar='<config_file_path>')
+ parser.add_argument('-c', '--config', dest='config',
+ action='store',
+ help='NFVbench config to execute (json format)',
+ metavar='<config>')
+ parser.add_argument('-e', '--echo', dest='echo',
+ action='store',
+ help='NFVbench config to echo (json format)',
+ metavar='<config>')
+ parser.add_argument('-t', '--timeout', dest='timeout',
+ default=900,
+ action='store',
+ help='time (seconds) to wait for NFVbench result',
+ metavar='<config>')
+ parser.add_argument('--use-socketio', dest='use_socketio',
+ action='store_true',
+ help='NFVbench config to echo (json format)')
+ parser.add_argument('url', help='nfvbench server url (e.g. http://10.0.0.1:5000)')
+ opts = parser.parse_args()
+
+ if not opts.file and not opts.config and not opts.echo:
+ print('at least one of -f or -c or -e required')
+ sys.exit(-1)
+
+ nfvbench = NfvbenchClient(opts.url, opts.use_socketio)
+ # convert JSON into a dict
+ try:
+ timeout = int(opts.timeout)
+ if opts.file:
+ with open(opts.file) as fd:
+ config = json.loads(fd.read())
+ result = nfvbench.run_config(config, timeout=timeout)
+ elif opts.config:
+ config = json.loads(opts.config)
+ result = nfvbench.run_config(config, timeout=timeout)
+ elif opts.echo:
+ config = json.loads(opts.echo)
+ result = nfvbench.echo_config(config, timeout=timeout)
+ print('Result:', result)
+ except ValueError as ex:
+ print('Input configuration is invalid: ' + str(ex))
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/client/requirements.txt b/client/requirements.txt
new file mode 100644
index 0000000..80fc402
--- /dev/null
+++ b/client/requirements.txt
@@ -0,0 +1,7 @@
+#
+#
+backports.ssl-match-hostname==3.5.0.1 # via websocket-client
+requests==2.13.0 # via socketio-client
+six==1.10.0 # via socketio-client, websocket-client
+socketIO-client==0.7.2
+websocket-client==0.40.0 # via socketio-client
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/conftest.py
diff --git a/docker/cleanup_generators.py b/docker/cleanup_generators.py
new file mode 100755
index 0000000..db68dcb
--- /dev/null
+++ b/docker/cleanup_generators.py
@@ -0,0 +1,78 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import shutil
+
+TREX_OPT = '/opt/trex'
+
+
+TREX_UNUSED = [
+ '_t-rex-64-debug', '_t-rex-64-debug-o', 'bp-sim-64', 'bp-sim-64-debug',
+ 't-rex-64-debug', 't-rex-64-debug-o', 'automation/__init__.py',
+ 'automation/graph_template.html',
+ 'automation/config', 'automation/h_avc.py', 'automation/phantom',
+ 'automation/readme.txt', 'automation/regression', 'automation/report_template.html',
+ 'automation/sshpass.exp', 'automation/trex_perf.py', 'wkhtmltopdf-amd64'
+]
+
+
+def remove_unused_libs(path, files):
+ """
+ Remove files not used by traffic generator.
+ """
+ for f in files:
+ f = os.path.join(path, f)
+ try:
+ if os.path.isdir(f):
+ shutil.rmtree(f)
+ else:
+ os.remove(f)
+ except OSError:
+ print "Skipped file:"
+ print f
+ continue
+
+
+def get_dir_size(start_path='.'):
+ """
+ Computes size of directory.
+
+ :return: size of directory with subdirectiories
+ """
+ total_size = 0
+ for dirpath, dirnames, filenames in os.walk(start_path):
+ for f in filenames:
+ try:
+ fp = os.path.join(dirpath, f)
+ total_size += os.path.getsize(fp)
+ except OSError:
+ continue
+ return total_size
+
+if __name__ == "__main__":
+ versions = os.listdir(TREX_OPT)
+ for version in versions:
+ trex_path = os.path.join(TREX_OPT, version)
+ print 'Cleaning TRex', version
+ try:
+ size_before = get_dir_size(trex_path)
+ remove_unused_libs(trex_path, TREX_UNUSED)
+ size_after = get_dir_size(trex_path)
+ print '==== Saved Space ===='
+ print size_before - size_after
+ except OSError:
+ import traceback
+ print traceback.print_exc()
+ print 'Cleanup was not finished.'
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..2d4ff46
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,225 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) testing/user/userguide
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) testing/user/userguide
+
+.PHONY: help
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NFVBench.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NFVBench.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/NFVBench"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NFVBench"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
diff --git a/docs/development/design/design.rst b/docs/development/design/design.rst
new file mode 100644
index 0000000..6717fd7
--- /dev/null
+++ b/docs/development/design/design.rst
@@ -0,0 +1,10 @@
+
+
+.. contents::
+ :depth: 3
+ :local:
+
+Introduction
+----------------
+
+
diff --git a/docs/development/design/index.rst b/docs/development/design/index.rst
new file mode 100644
index 0000000..22e79d9
--- /dev/null
+++ b/docs/development/design/index.rst
@@ -0,0 +1,10 @@
+.. To be decided
+
+=============================================
+OPNFV NFVbench Euphrates Design
+=============================================
+
+.. toctree::
+ :maxdepth: 1
+
+ design
diff --git a/docs/development/index.rst b/docs/development/index.rst
new file mode 100644
index 0000000..1dd34cc
--- /dev/null
+++ b/docs/development/index.rst
@@ -0,0 +1,10 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+.. toctree::
+ :maxdepth: 1
+
+ overview/index
+ design/index
diff --git a/docs/development/overview/index.rst b/docs/development/overview/index.rst
new file mode 100644
index 0000000..ce99621
--- /dev/null
+++ b/docs/development/overview/index.rst
@@ -0,0 +1,13 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+=============================================
+OPNFV NFVbench Euphrates Overview
+=============================================
+
+.. toctree::
+ :maxdepth: 1
+
+ overview
diff --git a/docs/development/overview/overview.rst b/docs/development/overview/overview.rst
new file mode 100644
index 0000000..2b86da3
--- /dev/null
+++ b/docs/development/overview/overview.rst
@@ -0,0 +1,13 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+.. contents::
+ :depth: 3
+ :local:
+
+Introduction
+----------------
+Describing the components and behaviours in a manner that helps people understand the platform and how to work with it
+
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..720b670
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,11 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+.. toctree::
+ :maxdepth: 1
+
+ development/index
+ release/release-notes/index
+ testing/index
diff --git a/docs/release/release-notes/index.rst b/docs/release/release-notes/index.rst
new file mode 100644
index 0000000..db59cfa
--- /dev/null
+++ b/docs/release/release-notes/index.rst
@@ -0,0 +1,11 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+
+****************************
+NFVbench Release Notes
+****************************
+
+.. toctree::
+ :maxdepth: 1
+
+ release-notes
diff --git a/docs/release/release-notes/release-notes.rst b/docs/release/release-notes/release-notes.rst
new file mode 100644
index 0000000..42b2cd4
--- /dev/null
+++ b/docs/release/release-notes/release-notes.rst
@@ -0,0 +1,59 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+OPNFV Euphrates Release
+=======================
+
+This is the introductory release for NFVbench. In this release, NFVbench provides the following features/capabilities:
+
+- standalone installation with a single Docker container integrating the open source TRex traffic generator
+- can measure data plane performance for any NFVi full stack
+- can setup automatically service chains with the following packet paths:
+ - PVP (physical-VM-physical)
+ - PVVP (physical-VM-VM-physical) intra-node and inter-node
+- can setup multiple service chains
+ - N * PVP
+ - N * PVVP
+- supports any external service chain (pre-set externally) that can do basic IPv4 routing
+- can measure
+ - drop rate and latency for any given fixed rate
+ - NDR (No Drop Rate) and PDR (Partial Drop Rate) with configurable drop rates
+- traffic specification
+ - any fixed frame size or IMIX
+ - uni or bidirectional traffic
+ - any number of flows
+ - vlan tagging can be enabled or disabled
+- user interface:
+ - CLI
+ - REST+socketIO
+- fully configurable runs with yaml-JSON configuration
+- detailed results in JSON format
+- summary tabular results
+
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------+
+| **Project** | opnfv/nfvbench |
+| | |
++--------------------------------------+--------------------------------------+
+| **Repo/commit-ID** | opnfv/nfvbench at tag 1.0.0 |
+| | |
++--------------------------------------+--------------------------------------+
+| **Release designation** | |
+| | |
++--------------------------------------+--------------------------------------+
+| **Release date** | |
+| | |
++--------------------------------------+--------------------------------------+
+| **Purpose of the delivery** | |
+| | |
++--------------------------------------+--------------------------------------+
+
+
+
+
+
+
diff --git a/docs/testing/developer/devguide/index.rst b/docs/testing/developer/devguide/index.rst
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/testing/developer/devguide/index.rst
diff --git a/docs/testing/index.rst b/docs/testing/index.rst
new file mode 100644
index 0000000..0b55d48
--- /dev/null
+++ b/docs/testing/index.rst
@@ -0,0 +1,11 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+.. toctree::
+ :maxdepth: 1
+
+ developer/devguide/index
+ user/configguide/index
+ user/userguide/index
diff --git a/docs/testing/user/configguide/configguide.rst b/docs/testing/user/configguide/configguide.rst
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/testing/user/configguide/configguide.rst
diff --git a/docs/testing/user/configguide/index.rst b/docs/testing/user/configguide/index.rst
new file mode 100644
index 0000000..ce3b778
--- /dev/null
+++ b/docs/testing/user/configguide/index.rst
@@ -0,0 +1,9 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International
+.. License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) Cisco Systems, Inc
+
+.. toctree::
+ :maxdepth: 1
+
+ configguide
diff --git a/docs/testing/user/userguide/_static/custom.css b/docs/testing/user/userguide/_static/custom.css
new file mode 100644
index 0000000..6cbfde3
--- /dev/null
+++ b/docs/testing/user/userguide/_static/custom.css
@@ -0,0 +1,4 @@
+.wy-nav-content {
+ max-width: 1200px !important;
+}
+
diff --git a/docs/testing/user/userguide/_templates/layout.html b/docs/testing/user/userguide/_templates/layout.html
new file mode 100644
index 0000000..f3387d5
--- /dev/null
+++ b/docs/testing/user/userguide/_templates/layout.html
@@ -0,0 +1,5 @@
+{% extends "!layout.html" %}
+{% block extrahead %}
+ <link href="{{ pathto("_static/custom.css", True) }}" rel="stylesheet" type="text/css">
+{% endblock %}
+
diff --git a/docs/testing/user/userguide/advanced.rst b/docs/testing/user/userguide/advanced.rst
new file mode 100644
index 0000000..f757b46
--- /dev/null
+++ b/docs/testing/user/userguide/advanced.rst
@@ -0,0 +1,318 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+==============
+Advanced Usage
+==============
+
+This section covers a few examples on how to run NFVbench with multiple different settings.
+Below are shown the most common and useful use-cases and explained some fields from a default config file.
+
+How to change any NFVbench run configuration (CLI)
+--------------------------------------------------
+NFVbench always starts with a default configuration which can further be partially refined (overridden) by the user from the CLI or from REST requests.
+
+At first have a look at the default config:
+
+.. code-block:: bash
+
+ nfvbench --show-default-config
+
+It is sometimes useful derive your own configuration from a copy of the default config:
+
+.. code-block:: bash
+
+ nfvbench --show-default-config > nfvbench.cfg
+
+At this point you can edit the copy by:
+
+- removing any parameter that is not to be changed (since NFVbench will always load the default configuration, default values are not needed)
+- edit the parameters that are to be changed changed
+
+A run with the new confguration can then simply be requested using the -c option and by using the actual path of the configuration file
+as seen from inside the container (in this example, we assume the current directory is mapped to /tmp/nfvbench in the container):
+
+.. code-block:: bash
+
+ nfvbench -c /tmp/nfvbench/nfvbench.cfg
+
+The same -c option also accepts any valid yaml or json string to override certain parameters without having to create a configuration file.
+
+NFVbench also provides many configuration options as optional arguments. For example the number of flows can be specified using the --flow-count option.
+
+For example, flow count can be specified in any of 3 ways:
+
+- by providing a confguration file that has the flow_count value to use (-c myconfig.yaml and myconfig.yaml contains 'flow_count: 100k')
+- by passing that yaml paremeter inline (-c "flow_count: 100k") or (-c "{flow_count: 100k}")
+- by using the flow count optional argument (--flow-count 100k)
+
+Showing the running configuration
+---------------------------------
+
+Because configuration parameters can be overriden, it is sometimes useful to show the final configuration (after all oevrrides are done) by using the --show-config option.
+This final configuration is also called the "running" configuration.
+
+For example, this will only display the running configuration (without actually running anything):
+
+.. code-block:: bash
+
+ nfvbench -c "{flow_count: 100k, debug: true}" --show-config
+
+
+Connectivity and Configuration Check
+------------------------------------
+
+NFVbench allows to test connectivity to devices used with selected flow test, for example PVP.
+It runs the whole test, but without actually sending any traffic or influencing interface counters.
+It is also a good way to check if everything is configured properly in the config file and what versions of components are used.
+
+
+To verify everything works without sending any traffic, use the --no-traffic option:
+
+.. code-block:: bash
+
+ nfvbench --no-traffic
+
+Used parameters:
+
+* ``--no-traffic`` or ``-0`` : sending traffic from traffic generator is skipped
+
+
+
+Fixed Rate Run
+--------------
+
+Fixed rate run is the most basic type of NFVbench usage. It is usually used to verify that some amount of packets can pass network components in selected flow.
+
+The first example shows how to run PVP flow (default flow) with multiple different settings:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg --no-reset --no-cleanup --rate 100000pps --duration 30 --interval 15 --json results.json
+
+Used parameters:
+
+* ``-c nfvbench.cfg`` : path to the config file
+* ``--no-cleanup`` : resources (networks, VMs, attached ports) are not deleted after test is finished
+* ``--rate 100000pps`` : defines rate of packets sent by traffic generator
+* ``--duration 30`` : specifies how long should traffic be running in seconds
+* ``--interval 15`` : stats are checked and shown periodically (in seconds) in this interval when traffic is flowing
+* ``--json results.json`` : collected data are stored in this file after run is finished
+
+.. note:: It is your responsibility to clean up resources if needed when ``--no-cleanup`` parameter is used.
+
+The ``--json`` parameter makes it easy to store NFVbench results. To display collected results in a table form, do:
+
+.. code-block:: bash
+
+ nfvbench --show-summary results.json # or shortcut -ss results.json
+
+
+Second example aims to show how to specify which supported flow to run:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg --rate 1Mbps --inter-node --service-chain PVVP
+
+Used parameters:
+
+* ``-c nfvbench.cfg`` : path to the config file
+* ``--rate 1Mbps`` : defines rate of packets sent by traffic generator
+* ``--inter-node`` : VMs are created on different compute nodes, works only with PVVP flow
+* ``--service-chain PVVP`` or ``-sc PVVP`` : specifies type of flow to use, default is PVP
+
+.. note:: When parameter ``--inter-node`` is not used or there aren't enough compute nodes, VMs are on the same compute node.
+
+
+Rate Units
+^^^^^^^^^^
+
+Parameter ``--rate`` accepts different types of values:
+
+* packets per second (pps, kpps, mpps), e.g. ``1000pps`` or ``10kpps``
+* load percentage (%), e.g. ``50%``
+* bits per second (bps, kbps, Mbps, Gbps), e.g. ``1Gbps``, ``1000bps``
+* NDR/PDR (ndr, pdr, ndr_pdr), e.g. ``ndr_pdr``
+
+The last mentioned value, NDR/PDR, is default one and its usage is covered more below.
+
+
+NDR and PDR
+-----------
+
+NDR and PDR test is used to determine performance of your setup, maximum packets throughput.
+
+* NDR (No Drop Rate): how many packets can be sent so (almost) none of them are dropped
+* PDR (Partial Drop Rate): how many packets can be sent so drop rate is below given limit
+
+Config file contains section where settings for NDR/PDR can be set.
+Increasing number of attempts helps to minimize a chance of traffic hiccups influencing result.
+Other way of increasing precision is to specify longer duration for traffic to run.
+
+.. code-block:: bash
+
+ # NDR/PDR configuration
+ measurement:
+ # Drop rates represent the ratio of dropped packet to the total number of packets sent.
+ # Values provided here are percentages. A value of 0.01 means that at most 0.01% of all
+ # packets sent are dropped (or 1 packet every 10,000 packets sent)
+
+ # No Drop Rate; Default to 0.001%
+ NDR: 0.001
+ # Partial Drop Rate; NDR should always be less than PDR
+ PDR: 0.1
+ # The accuracy of NDR and PDR load percentiles; The actual load percentile that match NDR
+ # or PDR should be within `load_epsilon` difference than the one calculated.
+ load_epsilon: 0.1
+
+Because NDR/PDR is the default ``--rate`` value, it's possible to run NFVbench simply like this:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg
+
+Other custom run:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg --duration 120 --json results.json
+
+Used parameters:
+
+* ``-c nfvbench.cfg`` : path to the config file
+* ``--duration 120`` : specifies how long should be traffic running in each iteration
+* ``--json results.json`` : collected data are stored in this file after run is finished
+
+
+Multichain
+----------
+
+NFVbench allows to run multiple chains at the same time. For example it is possible to run PVP service chain N-times,
+where N can be as much as your compute power can scale. With N = 10, NFVbench will spawn 10 VMs as a part of 10 simultaneous PVP chains.
+
+Number of chains is specified by ``--service-chain-count`` or ``-scc`` flag, default value is 1.
+For example to run NFVbench with 3 PVP chains use command:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg --rate 10000pps -scc 3
+
+It is not necessary to specify service chain because PVP is set as default. PVP service chains will have 3 VMs in 3 chains with this configuration.
+If ``-sc PVVP`` is specified instead, there would be 6 VMs in 3 chains as this service chain has 2 VMs per chain.
+Both **single run** or **NDR/PDR** can be run as multichain. Running multichain is a scenario closer to a real life situation than just simple run.
+
+
+External Chain
+--------------
+
+NFVbench can measure the performance of 1 or more L3 service chains that are setup externally. Instead of being setup by NFVbench,
+the complete environment (VMs and networks) has to be setup prior to running NFVbench.
+
+Each external chain is made of 1 or more VNFs and has exactly 2 end network interfaces (left and right network interfaces) that are connected to 2 neutron networks (left and right networks).
+The internal composition of a multi-VNF service chain can be arbitrary (usually linear) as far as NFVbench is concerned,
+the only requirement is that the service chain can route L3 packets properly between the left and right networks.
+
+To run NFVbench on such external service chains:
+
+- explicitly tell NFVbench to use external service chain by adding ``-sc EXT`` or ``--service-chain EXT`` to NFVbench CLI options
+- specify the number of external chains using the ``-scc`` option (defaults to 1 chain)
+- specify the 2 end point networks of your environment in ``external_networks`` inside the config file.
+ - The two networks specified there have to exist in Neutron and will be used as the end point networks by NFVbench ('napa' and 'marin' in the diagram below)
+- specify the router gateway IPs for the external service chains (1.1.0.2 and 2.2.0.2)
+- specify the traffic generator gateway IPs for the external service chains (1.1.0.102 and 2.2.0.102 in diagram below)
+- specify the packet source and destination IPs for the virtual devices that are simulated (10.0.0.0/8 and 20.0.0.0/8)
+
+
+.. image:: images/extchain-config.svg
+
+The L3 router function must be enabled in the VNF and configured to:
+
+- reply to ARP requests to its public IP addresses on both left and right networks
+- route packets from each set of remote devices toward the appropriate dest gateway IP in the traffic generator using 2 static routes (as illustrated in the diagram)
+
+Upon start, NFVbench will:
+- first retrieve the properties of the left and right networks using Neutron APIs,
+- extract the underlying network ID (either VLAN ID or VNI if VxLAN is used),
+- then program the TOR to stitch the 2 interfaces from the traffic generator into each end of the service chain,
+- then generate and measure traffic.
+
+Note that in the case of multiple chains, all chains end interfaces must be connected to the same two left and right networks.
+The traffic will be load balanced across the corresponding gateway IP of these external service chains.
+
+.. note:: By default, interfaces configuration (TOR, VTS, etc.) will be run by NFVbench but these can be skipped by using ``--no-int-config`` flag.
+
+
+Multiflow
+---------
+
+NFVbench always generates L3 packets from the traffic generator but allows the user to specify how many flows to generate.
+A flow is identified by a unique src/dest MAC IP and port tuple that is sent by the traffic generator. Note that from a vswitch point of view, the
+number of flows seen will be higher as it will be at least 4 times the number of flows sent by the traffic generator
+(add reverse direction of vswitch to traffic generator, add flow to VM and flow from VM).
+
+
+The number of flows will be spread roughly even between chains when more than 1 chain is being tested.
+For example, for 11 flows and 3 chains, number of flows that will run for each chain will be 3, 4, and 4 flows respectively.
+
+The number of flows is specified by ``--flow-count`` or ``-fc`` flag, the default value is 2 (1 flow in each direction).
+To run NFVbench with 3 chains and 100 flows, use the following command:
+
+.. code-block:: bash
+
+ nfvbench -c nfvbench.cfg --rate 10000pps -scc 3 -fc 100
+
+
+IP addresses generated can be controlled with the following NFVbench configuration options:
+
+.. code-block:: bash
+
+ ip_addrs: ['10.0.0.0/8', '20.0.0.0/8']
+ ip_addrs_step: 0.0.0.1
+ tg_gateway_ip_addrs: ['1.1.0.100', '2.2.0.100']
+ tg_gateway_ip_addrs_step: 0.0.0.1
+ gateway_ip_addrs: ['1.1.0.2', '2.2.0.2']
+ gateway_ip_addrs_step: 0.0.0.1
+
+``ip_addrs`` are the start of the 2 ip address ranges used by the traffic generators as the packets source and destination packets
+where each range is associated to virtual devices simulated behind 1 physical interface of the traffic generator.
+These can also be written in CIDR notation to represent the subnet.
+
+``tg_gateway_ip_addrs`` are the traffic generator gateway (virtual) ip addresses, all traffic to/from the virtual devices go through them.
+
+``gateway_ip_addrs`` are the 2 gateway ip address ranges of the VMs used in the external chains. They are only used with external chains and must correspond to their public IP address.
+
+The corresponding ``step`` is used for ranging the IP addresses from the `ip_addrs``, ``tg_gateway_ip_addrs`` and ``gateway_ip_addrs`` base addresses.
+0.0.0.1 is the default step for all IP ranges. In ``ip_addrs``, 'random' can be configured which tells NFVBench to generate random src/dst IP pairs in the traffic stream.
+
+
+Traffic Config via CLI
+----------------------
+
+While traffic configuration can modified using the config file, it became a hassle to have to change the config file everytime you need to change traffic config.
+
+Traffic config can be overridden with the CLI options.
+
+Here is an example of configuring traffic via CLI:
+
+.. code-block:: bash
+
+ nfvbench --rate 10kpps --service-chain-count 2 -fs 64 -fs IMIX -fs 1518 --unidir
+
+This command will run NFVbench with two streams with unidirectional flow for three packet sizes 64B, IMIX, and 1518B.
+
+Used parameters:
+
+* ``--rate 10kpps`` : defines rate of packets sent by traffic generator (total TX rate)
+* ``-scc 2`` or ``--service-chain-count 2`` : specifies number of parallel chains of given flow to run (default to 1)
+* ``-fs 64`` or ``--frame-size 64``: add the specified frame size to the list of frame sizes to run
+* ``--unidir`` : run traffic with unidirectional flow (default to bidirectional flow)
+
+
+MAC Addresses
+-------------
+
+NFVbench will dicover the MAC addresses to use for generated frames using:
+- either OpenStack discovery (find the MAC of an existing VM) if the loopback VM is configured to run L2 forwarding
+- or using dynamic ARP discovery (find MAC from IP) if the loopback VM is configured to run L3 routing or in the case of external chains.
+
diff --git a/docs/testing/user/userguide/conf.py b/docs/testing/user/userguide/conf.py
new file mode 100644
index 0000000..638764c
--- /dev/null
+++ b/docs/testing/user/userguide/conf.py
@@ -0,0 +1,344 @@
+# -*- coding: utf-8 -*-
+#
+# NFVBench documentation build configuration file, created by
+# sphinx-quickstart on Thu Sep 29 14:25:18 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+from pbr import version as pbr_ver
+
+sys.path.insert(0, os.path.abspath('../..'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'NFVBench'
+copyright = u'2016 Cisco Systems, Inc.'
+author = u'Cisco Systems, Inc.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = pbr_ver.VersionInfo(project).version_string()
+# The full version, including alpha/beta/rc tags.
+release = pbr_ver.VersionInfo(project).version_string_with_vcs()
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+#html_theme = 'haiku'
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# "<project> v<release> documentation" by default.
+#
+# html_title = u'NFVBench vdev117'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+html_short_title = 'nfvbench'
+
+# The name of an image file (relative to this directory) to place at the topß
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+html_use_smartypants = False
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'NFVBenchdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'NFVBench.tex', u'NFVBench Documentation',
+ u'Alec Hothan, Stefano Suryanto, Jan Balaz', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# It false, will not define \strong, \code, itleref, \crossref ... but only
+# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
+# packages.
+#
+# latex_keep_old_macro_names = True
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'nfvbench', u'NFVBench Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'NFVBench', u'NFVBench Documentation',
+ author, 'NFVBench', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
diff --git a/docs/testing/user/userguide/examples.rst b/docs/testing/user/userguide/examples.rst
new file mode 100644
index 0000000..4fc68b7
--- /dev/null
+++ b/docs/testing/user/userguide/examples.rst
@@ -0,0 +1,9 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+Example of Results
+******************
+
+
+
diff --git a/docs/testing/user/userguide/faq.rst b/docs/testing/user/userguide/faq.rst
new file mode 100644
index 0000000..cb5acb5
--- /dev/null
+++ b/docs/testing/user/userguide/faq.rst
@@ -0,0 +1,28 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+Frequently Asked Questions
+**************************
+
+
+Can NFVbench be used with a different traffic generator than TRex?
+------------------------------------------------------------------
+This is possible but requires developing a new python class to manage the new traffic generator interface.
+
+Can I connect Trex directly to my compute node?
+-----------------------------------------------
+That is possible but you will not be able to run more advanced use cases such as PVVP inter-node which requires 2 compute nodes.
+
+
+Can I drive NFVbench using a REST interface?
+--------------------------------------------
+NFVbench can run in server mode and accept HTTP or WebSocket/SocketIO events to run any type of measurement (fixed rate run or NDR_PDR run)
+with any run configuration.
+
+
+Can I run NFVbench on a Cisco UCS-B series blade?
+-------------------------------------------------
+Yes provided your UCS-B series server has a Cisco VIC 1340 (with a recent firmware version).
+TRex will require VIC firmware version 3.1(2) or higher for blade servers (which supports more filtering capabilities).
+In this setting, the 2 physical interfaces for data plane traffic are simply hooked to the UCS-B fabric interconnect (no need to connect to a switch).
diff --git a/docs/testing/user/userguide/hw_requirements.rst b/docs/testing/user/userguide/hw_requirements.rst
new file mode 100644
index 0000000..acb4c0a
--- /dev/null
+++ b/docs/testing/user/userguide/hw_requirements.rst
@@ -0,0 +1,79 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+Requirements for running NFVbench
+=================================
+
+.. _requirements:
+
+Hardware Requirements
+---------------------
+To run NFVbench you need the following hardware:
+- a Linux server
+- a DPDK compatible NIC with at least 2 ports (preferably 10Gbps or higher)
+- 2 ethernet cables between the NIC and the OpenStack pod under test (usually through a top of rack switch)
+
+The DPDK-compliant NIC must be one supported by the TRex traffic generator (such as Intel X710, refer to the `Trex Installation Guide <https://trex-tgn.cisco.com/trex/doc/trex_manual.html#_download_and_installation>`_ for a complete list of supported NIC)
+
+To run the TRex traffic generator (that is bundled with NFVbench) you will need to wire 2 physical interfaces of the NIC to the TOR switch(es):
+ - if you have only 1 TOR, wire both interfaces to that same TOR
+ - 1 interface to each TOR if you have 2 TORs and want to use bonded links to your compute nodes
+
+.. image:: images/nfvbench-trex-setup.svg
+
+
+Switch Configuration
+--------------------
+For VLAN encapsulation, the 2 corresponding ports on the switch(es) facing the Trex ports on the Linux server should be configured in trunk mode (NFVbench will instruct TRex to insert the appropriate vlan tag).
+
+For VxLAN encapsulation, the switch(es) must support the VTEP feature (VxLAN Tunnel End Point) with the ability to attach an interface to a VTEP (this is an advanced feature that requires an NFVbench plugin for the switch).
+
+Using a TOR switch is more representative of a real deployment and allows to measure packet flows on any compute node in the rack without rewiring and includes the overhead of the TOR switch.
+
+Although not the primary targeted use case, NFVbench could also support the direct wiring of the traffic generator to
+a compute node without a switch (although that will limit some of the features that invove multiple compute nodes in the packet path).
+
+Software Requirements
+---------------------
+
+You need Docker to be installed on the Linux server.
+
+TRex uses the DPDK interface to interact with the DPDK compatible NIC for sending and receiving frames. The Linux server will
+need to be configured properly to enable DPDK.
+
+DPDK requires a uio (User space I/O) or vfio (Virtual Function I/O) kernel module to be installed on the host to work.
+There are 2 main uio kernel modules implementations (igb_uio and uio_pci_generic) and one vfio kernel module implementation.
+
+To check if a uio or vfio is already loaded on the host:
+
+.. code-block:: bash
+
+ lsmod | grep -e igb_uio -e uio_pci_generic -e vfio
+
+
+If missing, it is necessary to install a uio/vfio kernel module on the host server:
+
+- find a suitable kernel module for your host server (any uio or vfio kernel module built with the same Linux kernel version should work)
+- load it using the modprobe and insmod commands
+
+Example of installation of the igb_uio kernel module:
+
+.. code-block:: bash
+
+ modprobe uio
+ insmod ./igb_uio.ko
+
+Finally, the correct iommu options and huge pages to be configured on the Linux server on the boot command line:
+
+- enable intel_iommu and iommu pass through: "intel_iommu=on iommu=pt"
+- for Trex, pre-allocate 1024 huge pages of 2MB each (for a total of 2GB): "hugepagesz=2M hugepages=1024"
+
+More detailed instructions can be found in the DPDK documentation (https://media.readthedocs.org/pdf/dpdk/latest/dpdk.pdf).
+
+
+NFVbench loopback VM image Upload
+---------------------------------
+
+The NFVbench loopback VM image should be uploaded to OpenStack prior to running NFVbench.
+The NFVbench VM qcow2 image can be rebuilt from script or can be copied from the OPNFV artifact repository [URL TBP].
diff --git a/docs/testing/user/userguide/images/extchain-config.svg b/docs/testing/user/userguide/images/extchain-config.svg
new file mode 100644
index 0000000..4e3db47
--- /dev/null
+++ b/docs/testing/user/userguide/images/extchain-config.svg
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="78 81 726 460" width="726pt" height="460pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.3
+ <dc:date>2017-03-31 20:15:29 +0000</dc:date>
+ </metadata>
+ <defs>
+ <filter id="Shadow" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in"/>
+ </filter>
+ <font-face font-family="Lucida Sans" font-size="10" panose-1="2 11 6 2 3 5 4 2 2 4" units-per-em="1000" underline-position="-97.65625" underline-thickness="48.828125" slope="0" x-height="541.9922" cap-height="740.7227" ascent="966.7969" descent="-210.9375" font-weight="500">
+ <font-face-src>
+ <font-face-name name="LucidaSans"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="10" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="Box_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="#cec3fa">
+ <g>
+ <rect x="0" y="-.9" width="1.8" height="1.8" fill="none" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="Box_Marker_2" viewBox="-3 -2 4 4" markerWidth="4" markerHeight="4" color="#cec3fa">
+ <g>
+ <rect x="-1.8" y="-.9" width="1.8" height="1.8" fill="none" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="Ball_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="#929292">
+ <g>
+ <circle cx=".8999996" cy="0" r=".8999992" fill="none" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="Ball_Marker_2" viewBox="-3 -2 4 4" markerWidth="4" markerHeight="4" color="#929292">
+ <g>
+ <circle cx="-.8999996" cy="0" r=".8999992" fill="none" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <font-face font-family="Gill Sans" font-size="10" panose-1="2 11 5 2 2 1 4 2 2 3" units-per-em="1000" underline-position="-75.19531" underline-thickness="49.80469" slope="0" x-height="449.70703" cap-height="687.0117" ascent="917.9688" descent="-230.46875" font-weight="500">
+ <font-face-src>
+ <font-face-name name="GillSans"/>
+ </font-face-src>
+ </font-face>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="#d4c057" opacity=".493177">
+ <g>
+ <path d="M 1.6 0 L 0 -.6 L 0 .6 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_2" viewBox="-3 -2 4 4" markerWidth="4" markerHeight="4" color="#d4c057" opacity=".493177">
+ <g>
+ <path d="M -1.6 0 L 0 .6 L 0 -.6 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.68359" underline-thickness="49.316406" slope="0" x-height="522.9492" cap-height="717.28516" ascent="770.0195" descent="-229.98047" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Helvetica"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Osaka" font-size="12" panose-1="2 11 6 0 0 0 0 0 0 0" units-per-em="1000" underline-position="0" underline-thickness="59.037876" slope="0" x-height="464.84375" cap-height="648.4375" ascent="855.4688" descent="-191.40625" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Osaka-Mono"/>
+ </font-face-src>
+ </font-face>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="#e86fd0">
+ <g>
+ <path d="M 8 0 L 0 0 M 0 -3 L 8 0 L 0 3" fill="none" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>ext chain</title>
+ <g>
+ <title>pvvp-intra</title>
+ <g>
+ <xl:use xl:href="#id49_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id48_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id46_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <g id="id49_Graphic">
+ <rect x="82.125" y="148.5" width="247.32675" height="182.87344" fill="#f0f0f0"/>
+ <rect x="82.125" y="148.5" width="247.32675" height="182.87344" stroke="#a5a5a5" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <text transform="translate(87.125 148.5)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="79.13457" y="10" textLength="79.05762">traffic generator</tspan>
+ </text>
+ </g>
+ <g id="id48_Graphic">
+ <rect x="479.25" y="167.625" width="176.625" height="151.875" fill="#f5f5eb"/>
+ <path d="M 479.25 167.625 L 655.875 167.625 L 655.875 319.5 L 479.25 319.5 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5" stroke-dasharray="4,4"/>
+ <text transform="translate(484.25 167.625)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="93.13867" y="10" textLength="73.48633">SERVICE CHAIN</tspan>
+ </text>
+ </g>
+ <line x1="507.125" y1="260" x2="507.125" y2="186.20833" stroke="#929292" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
+ <g id="id46_Graphic">
+ <rect x="379.125" y="166.5" width="57.68741" height="159.7121" fill="#d1d9e6" fill-opacity=".10672624"/>
+ <rect x="379.125" y="166.5" width="57.68741" height="159.7121" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <text transform="translate(384.125 166.5)" fill="black" fill-opacity=".4152397">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" fill-opacity=".4152397" x="7.4130395" y="10" textLength="22.27539">DC-S</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" fill-opacity=".4152397" x="29.6689" y="10" textLength="10.605469">W</tspan>
+ </text>
+ </g>
+ <line x1="274.14287" y1="292.33777" x2="626.52315" y2="293.47495" marker-end="url(#Box_Marker)" marker-start="url(#Box_Marker_2)" stroke="#cec3fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <line x1="631.3333" y1="308.625" x2="630" y2="212.625" stroke="#cec3fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
+ <line x1="273.76087" y1="205.73606" x2="502.52503" y2="206.58753" marker-end="url(#Ball_Marker)" marker-start="url(#Ball_Marker_2)" stroke="#929292" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <line x1="546.0999" y1="228.59572" x2="511.7247" y2="228.97873" marker-end="url(#Ball_Marker)" stroke="#929292" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <line x1="589.094" y1="243.43417" x2="625.8217" y2="243.0174" marker-end="url(#Box_Marker)" stroke="#cec3fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <ellipse cx="567.4375" cy="236.73324" rx="21.812535" ry="27.48328" fill="#eaf2bf"/>
+ <ellipse cx="567.4375" cy="236.73324" rx="21.812535" ry="27.48328" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(554.9875 225.73324)" fill="black">
+ <tspan font-family="Gill Sans" font-size="10" font-weight="500" fill="black" x="7.498828" y="9" textLength="12.680664">L3 </tspan>
+ <tspan font-family="Gill Sans" font-size="10" font-weight="500" fill="black" x="3.1799805" y="20" textLength="18.54004">VNF</tspan>
+ </text>
+ <ellipse cx="244.20895" cy="205.40873" rx="24.458986" ry="12.852811" fill="#eaf2bf"/>
+ <ellipse cx="244.20895" cy="205.40873" rx="24.458986" ry="12.852811" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(229.6418 198.7598)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="4.000751" y="10" textLength="21.132812">gwA</tspan>
+ </text>
+ <line x1="218.17016" y1="204.90873" x2="123.03817" y2="204.90873" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <line x1="124.875" y1="289.3752" x2="223.16667" y2="289.3752" stroke="#8e3bae" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <ellipse cx="245.08395" cy="290.853" rx="24.458986" ry="12.852811" fill="#eaf2bf"/>
+ <ellipse cx="245.08395" cy="290.853" rx="24.458986" ry="12.852811" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(230.5168 284.20406)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="4.747821" y="10" textLength="19.638672">gwB</tspan>
+ </text>
+ <path d="M 287.06743 305.7484 C 383.5292 303.84953 517.4412 309.08555 536.8379 307.16983 C 557.0879 305.16983 623.125 308.875 621.7917 272.875 C 620.4583 236.875 506.8675 259.29167 502.08333 252.625 C 497.29917 245.95833 494.08333 258.625 494.08333 229.95833 C 494.08333 201.29167 497.34544 216.1918 487.1968 204.85848 C 477.6795 194.23016 353.7978 198.7745 286.9911 197.26211" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="#d4c057" stroke-opacity=".493177" stroke-linecap="butt" stroke-linejoin="round" stroke-width="4"/>
+ <text transform="translate(545 483.125)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="10" textLength="67.59766">right public IP</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="22" textLength="34.785156">2.2.0.2</tspan>
+ </text>
+ <line x1="588.27516" y1="244.87632" x2="579.6301" y2="478.125" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(323.25 364.375)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".13378906" y="10" textLength="95.73242">right virtual devices</tspan>
+ </text>
+ <line x1="155.375" y1="283.97184" x2="342.5177" y2="358.875" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(677.75 127.625)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="10" textLength="63.291016">static routes:</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="22" textLength="120.07324">20.0.0.0/8 gw 2.2.0.102</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="34" textLength="120.07324">10.0.0.0/8 gw 1.1.0.102</tspan>
+ </text>
+ <line x1="586.2303" y1="222.76824" x2="692.92584" y2="168.625" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(478.375 360.5)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".234375" y="10" textLength="99.53125">right virtual gateway</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="26.28418" y="22" textLength="47.43164">2.2.0.102</tspan>
+ </text>
+ <line x1="269.5429" y1="292.32292" x2="473.49465" y2="355.5" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <rect x="100.26389" y="269.51805" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="100.26389" y="269.51805" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="96.81944" y="265.94663" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="96.81944" y="265.94663" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="93.375" y="262.3752" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="93.375" y="262.3752" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(98.375 271.80377)" fill="black">
+ <tspan font-family="Helvetica" font-size="10" font-weight="500" x=".3143446" y="10" textLength="44.48242">20.0.0.0/8</tspan>
+ <tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.6024306" y="22" textLength="33.90625">devices</tspan>
+ </text>
+ <rect x="101.38889" y="185.87466" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="101.38889" y="185.87466" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="97.94444" y="182.30323" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="97.94444" y="182.30323" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="94.5" y="178.7318" width="55.11111" height="42.857143" fill="white"/>
+ <rect x="94.5" y="178.7318" width="55.11111" height="42.857143" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(99.5 188.16037)" fill="black">
+ <tspan font-family="Helvetica" font-size="10" font-weight="500" x=".3143446" y="10" textLength="44.48242">10.0.0.0/8</tspan>
+ <tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.6024306" y="22" textLength="33.90625">devices</tspan>
+ </text>
+ <text transform="translate(375.125 86)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="45.3125" y="10" textLength="34.375">’marin’</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".4296875" y="22" textLength="124.14062">service chain left network</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="36.313477" y="34" textLength="52.373047">1.1.0.0/24</tspan>
+ </text>
+ <line x1="507.125" y1="192.32936" x2="455.72204" y2="127" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(609.125 423.5)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="10" textLength="29.873047">‘napa’</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="22" textLength="131.42578">service chain right network</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="0" y="34" textLength="52.373047">2.2.0.0/24</tspan>
+ </text>
+ <line x1="631.2763" y1="304.51667" x2="667.7626" y2="418.5" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(502 116.375)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".34375" y="10" textLength="60.3125">left public IP</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="13.107422" y="22" textLength="34.785156">1.1.0.2</tspan>
+ </text>
+ <line x1="546.5302" y1="228.09501" x2="534.89194" y2="145.375" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(250 95)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".37695312" y="10" textLength="92.24609">left virtual gateway</tspan>
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x="22.78418" y="22" textLength="47.43164">1.1.0.102</tspan>
+ </text>
+ <line x1="271.58333" y1="206.31494" x2="292.23495" y2="124" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(116.25 95.5)" fill="black">
+ <tspan font-family="Lucida Sans" font-size="10" font-weight="500" fill="black" x=".2763672" y="10" textLength="88.44727">left virtual devices</tspan>
+ </text>
+ <line x1="122.453" y1="178.7318" x2="155.04748" y2="113" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(97.25 396.85107)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="0" y="10" textLength="35.253906">nfvbenc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="35.512695" y="10" textLength="30.13672">h confi</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="65.649414" y="10" textLength="14.379883">g fi</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="80.0293" y="10" textLength="11.108398">le:</tspan>
+ </text>
+ <text transform="translate(91.625 425.75)" fill="black">
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="10" textLength="252">internal_network_name: [‘marin&apos;, ‘napa&apos;]</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="22" textLength="108">traffic_generator:</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="34" textLength="252"> ip_addrs: [&apos;10.0.0.0/8&apos;, &apos;20.0.0.0/8&apos;]</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="46" textLength="156"> ip_addrs_step: 0.0.0.1</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="58" textLength="318"> tg_gateway_ip_addrs: [&apos;1.1.0.102’, &apos;2.2.0.102’]</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="70" textLength="222"> tg_gateway_ip_addrs_step: 0.0.0.1</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="82" textLength="264"> gateway_ip_addrs: [&apos;1.1.0.2&apos;, &apos;2.2.0.2&apos;]</tspan>
+ <tspan font-family="Osaka" font-size="12" font-weight="500" x="0" y="94" textLength="204"> gateway_ip_addrs_step: 0.0.0.1</tspan>
+ </text>
+ <path d="M 604.125 438.8336 C 558.9773 439.34514 343.50462 360.4248 324.713 418.4743" marker-end="url(#StickArrow_Marker)" stroke="#e86fd0" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <path d="M 540 497.4063 C 494.85924 497.91775 348.4386 576.5024 327.58433 519.6221" marker-end="url(#StickArrow_Marker)" stroke="#e86fd0" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <path d="M 503.41667 393.95833 C 483.9034 443.0562 422.1928 531.9741 383.3793 497.97585" marker-end="url(#StickArrow_Marker)" stroke="#e86fd0" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <path d="M 366.45733 381.875 C 362.811 420.01984 368.47553 438.26776 347.53805 447.0305" marker-end="url(#StickArrow_Marker)" stroke="#e86fd0" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-npvp.svg b/docs/testing/user/userguide/images/nfvbench-npvp.svg
new file mode 100644
index 0000000..f72af34
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-npvp.svg
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="120 486 399 148" width="399pt" height="148pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2
+ <dc:date>2016-11-13 19:58:51 +0000</dc:date>
+ </metadata>
+ <defs>
+ <font-face font-family="Futura" font-size="15" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <filter id="Shadow" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in"/>
+ </filter>
+ <font-face font-family="Futura" font-size="11" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="10" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="12" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-flows 2</title>
+ <g>
+ <title>pvvp-intra</title>
+ <text transform="translate(129.0625 491.65665)" fill="black">
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x=".07739258" y="16" textLength="103.19092">N single VNF c</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="103.6565" y="16" textLength="35.266113">hains</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="16.000244" y="36.446777" textLength="62.13135">(N x PVP</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="77.06958" y="36.446777" textLength="45.930176">, N=2)</tspan>
+ </text>
+ </g>
+ <g>
+ <title>nxpvp</title>
+ <g>
+ <xl:use xl:href="#id38354_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38328_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38334_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <g id="id38354_Graphic">
+ <rect x="214.03778" y="541" width="57.68741" height="56.25" fill="#d1d9e6"/>
+ <rect x="214.03778" y="541" width="57.68741" height="56.25" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(219.03778 583.4224)" fill="black">
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="5.769973" y="11" textLength="24.50293">DC-S</tspan>
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="30.25142" y="11" textLength="11.666016">W</tspan>
+ </text>
+ </g>
+ <g id="id38328_Graphic">
+ <rect x="295.57012" y="505.6288" width="218.99238" height="121.75641" fill="#dae68e"/>
+ <rect x="295.57012" y="505.6288" width="218.99238" height="121.75641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(300.57012 614.58734)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="71.01718" y="10" textLength="20.927734">Com</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="92.06699" y="10" textLength="14.179688">put</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="106.33457" y="10" textLength="31.640625">e node</tspan>
+ </text>
+ </g>
+ <path d="M 352.57017 532.96637 L 402.2576 532.96637 C 404.4667 532.96637 406.2576 534.7572 406.2576 536.96637 L 406.2576 590.3585 C 406.2576 592.5677 404.4667 594.3585 402.2576 594.3585 L 352.57017 594.3585 C 350.36103 594.3585 348.57017 592.5677 348.57017 590.3585 L 348.57017 536.96637 C 348.57017 534.7572 350.36103 532.96637 352.57017 532.96637 Z" fill="#bbcee3"/>
+ <path d="M 352.57017 532.96637 L 402.2576 532.96637 C 404.4667 532.96637 406.2576 534.7572 406.2576 536.96637 L 406.2576 590.3585 C 406.2576 592.5677 404.4667 594.3585 402.2576 594.3585 L 352.57017 594.3585 C 350.36103 594.3585 348.57017 592.5677 348.57017 590.3585 L 348.57017 536.96637 C 348.57017 534.7572 350.36103 532.96637 352.57017 532.96637 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(353.57017 557.0135)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="7.566848" y="10" textLength="26.811523">vswitc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="34.63716" y="10" textLength="5.4833984">h</tspan>
+ </text>
+ <ellipse cx="463.7071" cy="536.9195" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="463.7071" cy="536.9195" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(443.9571 530.27055)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="6.5200195" y="10" textLength="26.45996">VNF1</tspan>
+ </text>
+ <ellipse cx="463.7071" cy="589.8623" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="463.7071" cy="589.8623" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(443.9571 583.2134)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="6.5200195" y="10" textLength="26.45996">VNF2</tspan>
+ </text>
+ <rect x="295.57012" y="505.6288" width="29.25" height="121.75641" fill="#eae9dd"/>
+ <rect x="295.57012" y="505.6288" width="29.25" height="121.75641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(300.57012 614.58734)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x=".42578125" y="10" textLength="18.398438">NIC</tspan>
+ </text>
+ <path d="M 169.92628 548.013 C 233.42628 548.013 333.63258 545.382 381.6326 538.382 C 429.6326 531.382 445.6882 531.18676 446.9104 537.68676 C 448.1326 544.18676 417.4888 544.1532 381.9888 549.6532 C 346.4888 555.1532 227.80128 558.76214 169.80128 558.1532" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <path d="M 170.41727 578.7688 C 233.91727 578.7688 334.12356 581.3998 382.12356 588.3998 C 430.12356 595.3998 446.0076 596.56276 446.7576 589.56276 C 447.5076 582.56276 417.97977 582.6286 382.47977 577.1286 C 346.97977 571.6286 228.29227 568.01965 170.29227 568.6286" stroke="#929292" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <g id="id38334_Graphic">
+ <rect x="124.0625" y="541" width="68.5" height="56.25" fill="#f0f0f0"/>
+ <rect x="124.0625" y="541" width="68.5" height="56.25" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(129.0625 553.7676)" fill="black">
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="13.983398" y="12" textLength="7.769531">tr</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="21.670898" y="12" textLength="17.126953">affi</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="38.79785" y="12" textLength="5.71875">c</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="2.3964844" y="27.357422" textLength="31.529297">gener</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="33.84375" y="27.357422" textLength="10.476562">at</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="44.402344" y="27.357422" textLength="11.701172">or</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-pvp.svg b/docs/testing/user/userguide/images/nfvbench-pvp.svg
new file mode 100644
index 0000000..e023b1f
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-pvp.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="114 61 405 103" width="405pt" height="103pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2
+ <dc:date>2016-11-13 19:58:51 +0000</dc:date>
+ </metadata>
+ <defs>
+ <font-face font-family="Futura" font-size="15" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <filter id="Shadow" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in"/>
+ </filter>
+ <font-face font-family="Futura" font-size="10" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="12" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-flows 2</title>
+ <g>
+ <title>pvvp-intra</title>
+ <text transform="translate(119.5625 67.15161)" fill="black">
+ <tspan font-family="Futura" font-size="15" font-weight="600" x=".2866211" y="16" textLength="85.89844">single VNF c</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" x="86.57324" y="16" textLength="71.14014">hain (PVP)</tspan>
+ </text>
+ </g>
+ <g>
+ <title>pvp</title>
+ <g>
+ <xl:use xl:href="#id38285_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38287_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38291_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <g id="id38285_Graphic">
+ <rect x="308.6875" y="72.34412" width="205.875" height="85.5" fill="#dae68e"/>
+ <rect x="308.6875" y="72.34412" width="205.875" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(313.6875 72.34412)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="64.458496" y="10" textLength="20.927734">Com</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="85.5083" y="10" textLength="14.179688">put</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="99.77588" y="10" textLength="31.640625">e node</tspan>
+ </text>
+ </g>
+ <rect x="308.6875" y="72.34412" width="29.25" height="85.5" fill="#eae9dd"/>
+ <rect x="308.6875" y="72.34412" width="29.25" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(313.6875 72.34412)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x=".42578125" y="10" textLength="18.398438">NIC</tspan>
+ </text>
+ <g id="id38287_Graphic">
+ <rect x="217.125" y="101.25" width="61.875" height="50.28088" fill="#d1d9e6"/>
+ <rect x="217.125" y="101.25" width="61.875" height="50.28088" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(222.125 101.25)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="9.506836" y="10" textLength="22.27539">DC-S</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="31.762695" y="10" textLength="10.605469">W</tspan>
+ </text>
+ </g>
+ <path d="M 360.4375 99.73456 L 399 99.73456 C 403.97056 99.73456 408 103.764 408 108.73456 L 408 141.01544 C 408 145.986 403.97056 150.01544 399 150.01544 L 360.4375 150.01544 C 355.46694 150.01544 351.4375 145.986 351.4375 141.01544 L 351.4375 108.73456 C 351.4375 103.764 355.46694 99.73456 360.4375 99.73456 Z" fill="#bbcee3"/>
+ <path d="M 360.4375 99.73456 L 399 99.73456 C 403.97056 99.73456 408 103.764 408 108.73456 L 408 141.01544 C 408 145.986 403.97056 150.01544 399 150.01544 L 360.4375 150.01544 C 355.46694 150.01544 351.4375 145.986 351.4375 141.01544 L 351.4375 108.73456 C 351.4375 103.764 355.46694 99.73456 360.4375 99.73456 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(356.4375 99.73456)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="7.0043945" y="10" textLength="26.811523">vswitc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="34.074707" y="10" textLength="5.4833984">h</tspan>
+ </text>
+ <ellipse cx="475.26203" cy="126.39044" rx="30.93755" ry="25.14048" fill="#eaf2bf"/>
+ <ellipse cx="475.26203" cy="126.39044" rx="30.93755" ry="25.14048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(455.51203 119.74152)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="9.605957" y="10" textLength="20.288086">VNF</tspan>
+ </text>
+ <path d="M 182.62498 119.31252 L 452.75 119.31252 C 455.8221 119.31252 458.3125 121.80293 458.3125 124.875 L 458.3125 124.875 C 458.3125 127.94707 455.8221 130.43748 452.75 130.43748 L 182.62498 130.43748 C 179.5529 130.43748 177.0625 127.94707 177.0625 124.875 L 177.0625 124.875 C 177.0625 121.80293 179.5529 119.31252 182.62498 119.31252 Z" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <g id="id38291_Graphic">
+ <rect x="125.0625" y="101.25" width="68.5" height="50.28088" fill="#f0f0f0"/>
+ <rect x="125.0625" y="101.25" width="68.5" height="50.28088" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(130.0625 111.03302)" fill="black">
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="13.983398" y="12" textLength="7.769531">tr</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="21.670898" y="12" textLength="17.126953">affi</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="38.79785" y="12" textLength="5.71875">c</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="2.3964844" y="27.357422" textLength="31.529297">gener</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="33.84375" y="27.357422" textLength="10.476562">at</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="44.402344" y="27.357422" textLength="11.701172">or</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-pvvp-inter.svg b/docs/testing/user/userguide/images/nfvbench-pvvp-inter.svg
new file mode 100644
index 0000000..3371346
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-pvvp-inter.svg
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="581 205 359 200" width="359pt" height="200pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2
+ <dc:date>2016-11-13 19:58:51 +0000</dc:date>
+ </metadata>
+ <defs>
+ <filter id="Shadow" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in"/>
+ </filter>
+ <font-face font-family="Futura" font-size="15" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="10" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="11" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="12" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-flows 2</title>
+ <g>
+ <title>pvvp-intra</title>
+ <g>
+ <xl:use xl:href="#id38374_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38375_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <text transform="translate(590.2153 212.05322)" fill="black">
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="17.86377" y="16" textLength="12.524414">2-</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="30.358887" y="16" textLength="42.209473">VNF c</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="72.95654" y="16" textLength="29.179688">hain</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x=".08056641" y="36.446777" textLength="21.555176">(int</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="21.767578" y="36.446777" textLength="13.908691">er</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="35.507812" y="36.446777" textLength="84.41162">-node PVVP)</tspan>
+ </text>
+ <g id="id38374_Graphic">
+ <rect x="764.0202" y="207" width="172.13258" height="85.5" fill="#dae68e"/>
+ <rect x="764.0202" y="207" width="172.13258" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(769.0202 207)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="42.340703" y="10" textLength="20.927734">Com</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="63.39051" y="10" textLength="14.179688">put</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="77.65809" y="10" textLength="42.13379">e node A</tspan>
+ </text>
+ </g>
+ <g id="id38375_Graphic">
+ <rect x="763.0202" y="312.3848" width="173.13258" height="85.5" fill="#dae68e"/>
+ <rect x="763.0202" y="312.3848" width="173.13258" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(768.0202 385.08695)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="43.587773" y="10" textLength="20.927734">Com</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="64.63758" y="10" textLength="14.179688">put</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" x="78.90516" y="10" textLength="40.63965">e node B</tspan>
+ </text>
+ </g>
+ </g>
+ <g>
+ <title>pvv-inter</title>
+ <g>
+ <xl:use xl:href="#id38306_Graphic" filter="url(#Shadow)"/>
+ <xl:use xl:href="#id38310_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <rect x="764.0202" y="207" width="29.25" height="85.5" fill="#eae9dd"/>
+ <rect x="764.0202" y="207" width="29.25" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(769.0202 207)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x=".42578125" y="10" textLength="18.398438">NIC</tspan>
+ </text>
+ <g id="id38306_Graphic">
+ <rect x="671.625" y="280.125" width="57.68741" height="56.25" fill="#d1d9e6"/>
+ <rect x="671.625" y="280.125" width="57.68741" height="56.25" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(676.625 301.08618)" fill="black">
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="5.769973" y="11" textLength="24.50293">DC-S</tspan>
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="30.25142" y="11" textLength="11.666016">W</tspan>
+ </text>
+ </g>
+ <path d="M 806.496 233.4918 L 843.4956 233.4918 C 846.257 233.4918 848.4956 235.73038 848.4956 238.4918 L 848.4956 278.7727 C 848.4956 281.5341 846.257 283.7727 843.4956 283.7727 L 806.496 283.7727 C 803.73455 283.7727 801.496 281.5341 801.496 278.7727 L 801.496 238.4918 C 801.496 235.73038 803.73455 233.4918 806.496 233.4918 Z" fill="#bbcee3"/>
+ <path d="M 806.496 233.4918 L 843.4956 233.4918 C 846.257 233.4918 848.4956 235.73038 848.4956 238.4918 L 848.4956 278.7727 C 848.4956 281.5341 846.257 283.7727 843.4956 283.7727 L 806.496 283.7727 C 803.73455 283.7727 801.496 281.5341 801.496 278.7727 L 801.496 238.4918 C 801.496 235.73038 803.73455 233.4918 806.496 233.4918 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(806.496 233.4918)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="2.2229413" y="10" textLength="26.811523">vswitc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="29.293254" y="10" textLength="5.4833984">h</tspan>
+ </text>
+ <ellipse cx="897.3403" cy="253.4073" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="897.3403" cy="253.4073" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(877.5903 238.4969)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="3.5219727" y="10" textLength="32.456055">VNF1a</tspan>
+ </text>
+ <ellipse cx="897.3403" cy="362.5323" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="897.3403" cy="362.5323" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(877.5903 364.64482)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="3.5219727" y="10" textLength="32.456055">VNF1b</tspan>
+ </text>
+ <rect x="762.9653" y="312.3848" width="29.25" height="85.5" fill="#eae9dd"/>
+ <rect x="762.9653" y="312.3848" width="29.25" height="85.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(767.9653 385.08695)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x=".42578125" y="10" textLength="18.398438">NIC</tspan>
+ </text>
+ <path d="M 807.4024 329.71157 L 842.0227 329.71157 C 844.7841 329.71157 847.0227 331.95015 847.0227 334.71157 L 847.0227 374.99245 C 847.0227 377.75388 844.7841 379.99245 842.0227 379.99245 L 807.4024 379.99245 C 804.641 379.99245 802.4024 377.75388 802.4024 374.99245 L 802.4024 334.71157 C 802.4024 331.95015 804.641 329.71157 807.4024 329.71157 Z" fill="#bbcee3"/>
+ <path d="M 807.4024 329.71157 L 842.0227 329.71157 C 844.7841 329.71157 847.0227 331.95015 847.0227 334.71157 L 847.0227 374.99245 C 847.0227 377.75388 844.7841 379.99245 842.0227 379.99245 L 807.4024 379.99245 C 804.641 379.99245 802.4024 377.75388 802.4024 374.99245 L 802.4024 334.71157 C 802.4024 331.95015 804.641 329.71157 807.4024 329.71157 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(807.4024 367.1946)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="1.0333076" y="10" textLength="26.811523">vswitc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="28.10362" y="10" textLength="5.4833984">h</tspan>
+ </text>
+ <path d="M 636.55465 318.22146 C 700.05465 318.22146 742.74215 337.67113 777.74215 345.67113 C 812.74215 353.67113 886.0206 366.93505 890.0206 355.62176 C 894.0206 344.30847 843.2908 342.9551 814.6797 339.17113 C 786.0687 335.38717 724.49215 318.90186 724.49215 307.17113" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <path d="M 636.7778 296.02245 C 700.2778 296.02245 742.9653 276.57278 777.9653 268.57278 C 812.9653 260.57278 886.0278 244.75948 890.0278 256.07278 C 894.0278 267.38607 841.4653 269.07278 814.9029 275.07278 C 788.3405 281.07278 724.7153 295.34205 724.7153 307.07278" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <g id="id38310_Graphic">
+ <rect x="585.2153" y="280.125" width="68.5" height="56.25" fill="#f0f0f0"/>
+ <rect x="585.2153" y="280.125" width="68.5" height="56.25" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(590.2153 292.89258)" fill="black">
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="13.983398" y="12" textLength="7.769531">tr</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="21.670898" y="12" textLength="17.126953">affi</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="38.79785" y="12" textLength="5.71875">c</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="2.3964844" y="27.357422" textLength="31.529297">gener</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="33.84375" y="27.357422" textLength="10.476562">at</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="44.402344" y="27.357422" textLength="11.701172">or</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-pvvp-intra.svg b/docs/testing/user/userguide/images/nfvbench-pvvp-intra.svg
new file mode 100644
index 0000000..6c454b2
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-pvvp-intra.svg
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="121 227 398 139" width="398pt" height="139pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2
+ <dc:date>2016-11-13 19:58:51 +0000</dc:date>
+ </metadata>
+ <defs>
+ <filter id="Shadow" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in"/>
+ </filter>
+ <filter id="Shadow_2" filterUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="1.308"/>
+ <feOffset in="blur" result="offset" dx="0" dy="2"/>
+ <feFlood flood-color="black" flood-opacity=".5" result="flood"/>
+ <feComposite in="flood" in2="offset" operator="in" result="color"/>
+ <feMerge>
+ <feMergeNode in="color"/>
+ <feMergeNode in="SourceGraphic"/>
+ </feMerge>
+ </filter>
+ <font-face font-family="Futura" font-size="10" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="11" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="15" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Futura" font-size="12" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="600">
+ <font-face-src>
+ <font-face-name name="Futura-Medium"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-flows 2</title>
+ <g>
+ <title>pvvp-intra</title>
+ <g>
+ <xl:use xl:href="#id38276_Graphic" filter="url(#Shadow)"/>
+ </g>
+ <g filter="url(#Shadow_2)">
+ <rect x="307.6875" y="233.82794" width="206.875" height="126.17206" fill="#dae68e"/>
+ <rect x="307.6875" y="233.82794" width="206.875" height="126.17206" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(312.6875 233.82794)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="64.958496" y="10" textLength="20.927734">Com</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="86.0083" y="10" textLength="14.179688">put</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="100.27588" y="10" textLength="31.640625">e node</tspan>
+ </text>
+ </g>
+ <rect x="307.6875" y="233.82794" width="29.25" height="126.17206" fill="#eae9dd"/>
+ <rect x="307.6875" y="233.82794" width="29.25" height="126.17206" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(312.6875 233.82794)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x=".42578125" y="10" textLength="18.398438">NIC</tspan>
+ </text>
+ <g filter="url(#Shadow_2)">
+ <rect x="217.125" y="271.77353" width="61.875" height="50.28088" fill="#d1d9e6"/>
+ <rect x="217.125" y="271.77353" width="61.875" height="50.28088" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(222.125 289.75015)" fill="black">
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="7.8637695" y="11" textLength="24.50293">DC-S</tspan>
+ <tspan font-family="Futura" font-size="11" font-weight="600" fill="black" x="32.345215" y="11" textLength="11.666016">W</tspan>
+ </text>
+ </g>
+ <path d="M 356.4375 272.44522 L 403 272.44522 C 405.7614 272.44522 408 274.6838 408 277.44522 L 408 317.7261 C 408 320.48753 405.7614 322.7261 403 322.7261 L 356.4375 322.7261 C 353.67608 322.7261 351.4375 320.48753 351.4375 317.7261 L 351.4375 277.44522 C 351.4375 274.6838 353.67608 272.44522 356.4375 272.44522 Z" fill="#bbcee3"/>
+ <path d="M 356.4375 272.44522 L 403 272.44522 C 405.7614 272.44522 408 274.6838 408 277.44522 L 408 317.7261 C 408 320.48753 405.7614 322.7261 403 322.7261 L 356.4375 322.7261 C 353.67608 322.7261 351.4375 320.48753 351.4375 317.7261 L 351.4375 277.44522 C 351.4375 274.6838 353.67608 272.44522 356.4375 272.44522 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(356.4375 290.93674)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="7.0043945" y="10" textLength="26.811523">vswitc</tspan>
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="34.074707" y="10" textLength="5.4833984">h</tspan>
+ </text>
+ <ellipse cx="475.26203" cy="262.05055" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="475.26203" cy="262.05055" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(455.51203 247.14017)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="3.5219727" y="10" textLength="32.456055">VNF1a</tspan>
+ </text>
+ <ellipse cx="475.26203" cy="333.4872" rx="30.93755" ry="21.300585" fill="#eaf2bf"/>
+ <ellipse cx="475.26203" cy="333.4872" rx="30.93755" ry="21.300585" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(455.51203 335.59974)" fill="black">
+ <tspan font-family="Futura" font-size="10" font-weight="600" fill="black" x="3.5219727" y="10" textLength="32.456055">VNF1b</tspan>
+ </text>
+ <text transform="translate(130.0625 232.5266)" fill="black">
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x=".31152344" y="16" textLength="12.524414">2-</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="12.80664" y="16" textLength="42.209473">VNF c</tspan>
+ <tspan font-family="Futura" font-size="15" font-weight="600" fill="black" x="55.4043" y="16" textLength="81.28418">hain (PVVP)</tspan>
+ </text>
+ <path d="M 182 307.60514 C 245.5 307.60514 348.18945 305.48077 386.18945 315.0207 C 424.18945 324.56065 461.68945 339.06065 465.18945 330.06065 C 468.68945 321.06065 436.68945 314.06065 422.68945 310.56065 C 408.68945 307.06065 402.87695 308.06065 402.68945 299.06065" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ <g id="id38276_Graphic">
+ <rect x="125.0625" y="272.44522" width="69.5" height="50.28088" fill="#f0f0f0"/>
+ <rect x="125.0625" y="272.44522" width="69.5" height="50.28088" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(130.0625 282.22824)" fill="black">
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="14.483398" y="12" textLength="7.769531">tr</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="22.170898" y="12" textLength="17.126953">affi</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="39.29785" y="12" textLength="5.71875">c</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="2.8964844" y="27.357422" textLength="31.529297">gener</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="34.34375" y="27.357422" textLength="10.476562">at</tspan>
+ <tspan font-family="Futura" font-size="12" font-weight="600" fill="black" x="44.902344" y="27.357422" textLength="11.701172">or</tspan>
+ </text>
+ </g>
+ </g>
+ <g>
+ <title>pvp</title>
+ <path d="M 195.06213 289.18618 C 258.56213 289.18618 348.18945 291.32988 386.18945 281.78994 C 424.18945 272.25 461.68945 257.75 465.18945 266.75 C 468.68945 275.75 436.68945 282.75 422.68945 286.25 C 408.68945 289.75 402.87695 288.75 402.68945 297.75" stroke="#aaa" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-spirent-setup.svg b/docs/testing/user/userguide/images/nfvbench-spirent-setup.svg
new file mode 100644
index 0000000..e149fc0
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-spirent-setup.svg
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="169 216 693 327" width="693pt" height="327pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2.1
+ <dc:date>2016-12-03 22:53:19 +0000</dc:date>
+ </metadata>
+ <defs>
+ <font-face font-family="Monaco" font-size="10" units-per-em="1000" underline-position="-37.597656" underline-thickness="75.68359" slope="0" x-height="560.5469" cap-height="780.2734" ascent="1e3" descent="-250" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Monaco"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Candara" font-size="12" panose-1="2 14 5 2 3 3 3 2 2 4" units-per-em="1000" underline-position="-64.94141" underline-thickness="9.765625" slope="0" x-height="463.8672" cap-height="638.6719" ascent="952.1484" descent="-268.5547" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Candara"/>
+ </font-face-src>
+ </font-face>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledBall_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="#847b5a">
+ <g>
+ <circle cx="2.9999986" cy="0" r="2.9999973" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledBall_Marker_2" viewBox="-7 -4 8 8" markerWidth="8" markerHeight="8" color="#847b5a">
+ <g>
+ <circle cx="-2.9999986" cy="0" r="2.9999973" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.68359" underline-thickness="49.316406" slope="0" x-height="522.9492" cap-height="717.28516" ascent="770.0195" descent="-229.98047" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Helvetica"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Helvetica Neue" font-size="12" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="bold">
+ <font-face-src>
+ <font-face-name name="HelveticaNeue-Bold"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Monaco" font-size="18" units-per-em="1000" underline-position="-37.597656" underline-thickness="75.68359" slope="0" x-height="560.5469" cap-height="780.2734" ascent="1e3" descent="-250" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Monaco"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-spirent-setup</title>
+ <g>
+ <title>Layer 1</title>
+ <rect x="169.875" y="310.268" width="183.375" height="156.607" fill="#afb2e0"/>
+ <rect x="169.875" y="310.268" width="183.375" height="156.607" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(174.875 310.268)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="32.67871" y="10" textLength="108.01758">Mercury Build Node</tspan>
+ </text>
+ <path d="M 279.875 334.19787 L 335.78906 334.19787 C 341.8642 334.19787 346.78906 339.12274 346.78906 345.19787 L 346.78906 386.125 C 346.78906 392.20013 341.8642 397.125 335.78906 397.125 L 279.875 397.125 C 273.79987 397.125 268.875 392.20013 268.875 386.125 L 268.875 345.19787 C 268.875 339.12274 273.79987 334.19787 279.875 334.19787 Z" fill="#eaf2bf"/>
+ <path d="M 279.875 334.19787 L 335.78906 334.19787 C 341.8642 334.19787 346.78906 339.12274 346.78906 345.19787 L 346.78906 386.125 C 346.78906 392.20013 341.8642 397.125 335.78906 397.125 L 279.875 397.125 C 273.79987 397.125 268.875 392.20013 268.875 386.125 L 268.875 345.19787 C 268.875 339.12274 273.79987 334.19787 279.875 334.19787 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <path d="M 191.01562 334.19787 L 242.14062 334.19787 C 248.21576 334.19787 253.14062 339.12274 253.14062 345.19787 L 253.14062 442.804 C 253.14062 448.87915 248.21576 453.804 242.14062 453.804 L 191.01562 453.804 C 184.9405 453.804 180.01562 448.87915 180.01562 442.804 L 180.01562 345.19787 C 180.01562 339.12274 184.9405 334.19787 191.01562 334.19787 Z" fill="#eaf2bf"/>
+ <path d="M 191.01562 334.19787 L 242.14062 334.19787 C 248.21576 334.19787 253.14062 339.12274 253.14062 345.19787 L 253.14062 442.804 C 253.14062 448.87915 248.21576 453.804 242.14062 453.804 L 191.01562 453.804 C 184.9405 453.804 180.01562 448.87915 180.01562 442.804 L 180.01562 345.19787 C 180.01562 339.12274 184.9405 334.19787 191.01562 334.19787 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="185.375" y="420.9982" width="62.40625" height="26.889958" fill="#e7d9fe"/>
+ <rect x="185.375" y="420.9982" width="62.40625" height="26.889958" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(185.375 427.4432)" fill="black">
+ <tspan font-family="Candara" font-size="12" font-weight="500" x="7.416992" y="11" textLength="47.572266">nfvbench</tspan>
+ </text>
+ <rect x="723.375" y="360.661" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="360.661" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 364.43103)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node 1</tspan>
+ </text>
+ <rect x="507.375" y="344.49533" width="73.125" height="45.305643" fill="#d1d9e6"/>
+ <rect x="507.375" y="344.49533" width="73.125" height="45.305643" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(512.375 360.23067)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="10.559082" y="10" textLength="42.006836">N9K TOR</tspan>
+ </text>
+ <rect x="191.26562" y="343.58694" width="50.625" height="43.69141" fill="#e7d9fe"/>
+ <rect x="191.26562" y="343.58694" width="50.625" height="43.69141" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(196.26562 358.51517)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="5.3100586" y="10" textLength="30.004883">T-rex</tspan>
+ </text>
+ <path d="M 254.78125 434.4432 L 256.78125 434.4432 L 296.8802 434.4432 L 296.8802 395.77836 L 296.8802 393.77836" marker-end="url(#FilledBall_Marker)" marker-start="url(#FilledBall_Marker_2)" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="372.67188" y="346.10956" width="62.40625" height="43.69141" fill="#eae9dd"/>
+ <rect x="372.67188" y="346.10956" width="62.40625" height="43.69141" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(377.67188 361.0378)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="5.199707" y="10" textLength="42.006836">Spirent</tspan>
+ </text>
+ <line x1="507.375" y1="359.5972" x2="435.0781" y2="360.67336" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="217.125" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="217.125" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 220.89502)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="288.893" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="288.893" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 292.66303)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <line x1="507.375" y1="374.6991" x2="435.0781" y2="375.23717" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="238.5" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="238.5" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 242.27002)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="259.875" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="259.875" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 263.64502)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="310.268" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="310.268" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 314.03803)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <rect x="723.375" y="331.643" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="331.643" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 335.41303)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <rect x="723.375" y="432.429" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="432.429" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 436.19904)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node i</tspan>
+ </text>
+ <rect x="507.375" y="286.33736" width="73.125" height="45.305643" fill="#d1d9e6"/>
+ <rect x="507.375" y="286.33736" width="73.125" height="45.305643" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(512.375 302.0727)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="10.559082" y="10" textLength="42.006836">N9K TOR</tspan>
+ </text>
+ <line x1="543.9375" y1="343.99533" x2="543.9375" y2="331.643" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"/>
+ <path d="M 723.375 227.8125 L 716.875 227.8125 L 651.875 227.8125 L 651.875 308.9902 L 587 308.9902 L 580.5 308.9902" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <path d="M 723.375 231.375 L 716.875 231.375 L 657 231.375 L 657 353.018 L 657 359.5972 L 587 359.5972 L 580.5 359.5972" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <path d="M 723.375 443.1165 L 715.375 443.1165 L 672 443.1165 L 672 417.66667 L 672 374.6991 L 588.5 374.6991 L 580.5 374.6991" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <path d="M 723.375 435.9915 L 715.375 435.9915 L 679.3333 435.9915 L 679.3333 382.036 L 679.3333 315 L 580.5 315" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="482.822" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="482.822" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 486.59205)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node N</tspan>
+ </text>
+ <path d="M 607.375 328.5 L 638 328.5 C 643.52285 328.5 648 332.97715 648 338.5 L 648 338.5 C 648 344.02285 643.52285 348.5 638 348.5 L 607.375 348.5 C 601.85215 348.5 597.375 344.02285 597.375 338.5 L 597.375 338.5 C 597.375 332.97715 601.85215 328.5 607.375 328.5 Z" fill="#e4e4e4"/>
+ <text transform="translate(607.375 331.5)" fill="#424242">
+ <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="#424242" x="3.977539" y="11" textLength="22.669922">vPC</tspan>
+ </text>
+ <path d="M 463.5 357.95526 L 470.5 357.95526 C 475.47056 357.95526 479.5 361.9847 479.5 366.95526 L 479.5 368.95526 C 479.5 373.92583 475.47056 377.95526 470.5 377.95526 L 463.5 377.95526 C 458.52944 377.95526 454.5 373.92583 454.5 368.95526 L 454.5 366.95526 C 454.5 361.9847 458.52944 357.95526 463.5 357.95526 Z" fill="#c75d5b"/>
+ <text transform="translate(463.5 360.28122)" fill="white">
+ <tspan font-family="Helvetica Neue" font-size="12" font-weight="bold" fill="white" x="0" y="12" textLength="6.672">1</tspan>
+ </text>
+ <path d="M 256.78125 439.875 L 263.78125 439.875 C 268.7518 439.875 272.78125 443.90444 272.78125 448.875 L 272.78125 450.875 C 272.78125 455.84556 268.7518 459.875 263.78125 459.875 L 256.78125 459.875 C 251.8107 459.875 247.78125 455.84556 247.78125 450.875 L 247.78125 448.875 C 247.78125 443.90444 251.8107 439.875 256.78125 439.875 Z" fill="#c75d5b"/>
+ <text transform="translate(256.78125 442.20096)" fill="white">
+ <tspan font-family="Helvetica Neue" font-size="12" font-weight="bold" fill="white" x="0" y="12" textLength="6.672">2</tspan>
+ </text>
+ <text transform="translate(308.75 514.29004)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x=".48828125" y="10" textLength="144.02344">pull spirent test center</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="3.4887695" y="23.83496" textLength="138.02246">and nfvbench containers</tspan>
+ </text>
+ <line x1="260.28125" y1="459.875" x2="303.75" y2="532.79167" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(472.875 414.775)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x=".4892578" y="10" textLength="132.02148">connect spirent to TOR</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="9.490723" y="23.83496" textLength="114.01855">with 2 x 10G cables</tspan>
+ </text>
+ <line x1="467" y1="377.95526" x2="493.875" y2="428.61" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(284.875 222.99854)" fill="black">
+ <tspan font-family="Monaco" font-size="18" font-weight="500" x=".18066406" y="18" textLength="237.63867">nfvbench/Spirent setup</tspan>
+ </text>
+ <rect x="276.07812" y="343.58694" width="62.40625" height="43.69141" fill="#e7d9fe"/>
+ <rect x="276.07812" y="343.58694" width="62.40625" height="43.69141" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(281.07812 344.6802)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="5.199707" y="10" textLength="42.006836">Spirent</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="14.201172" y="23.83496" textLength="30.004883">Test </tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="8.200195" y="37.66992" textLength="36.00586">Center</tspan>
+ </text>
+ <line x1="342.49312" y1="369.0503" x2="365.6765" y2="368.2091" marker-end="url(#FilledBall_Marker)" marker-start="url(#FilledBall_Marker_2)" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/images/nfvbench-trex-setup.svg b/docs/testing/user/userguide/images/nfvbench-trex-setup.svg
new file mode 100644
index 0000000..3f68006
--- /dev/null
+++ b/docs/testing/user/userguide/images/nfvbench-trex-setup.svg
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="227 216 635 295" width="635pt" height="295pt" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata> Produced by OmniGraffle 7.2.2
+ <dc:date>2017-03-17 17:57:44 +0000</dc:date>
+ </metadata>
+ <defs>
+ <font-face font-family="Monaco" font-size="10" units-per-em="1000" underline-position="-37.597656" underline-thickness="75.68359" slope="0" x-height="560.5469" cap-height="780.2734" ascent="1e3" descent="-250" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Monaco"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Candara" font-size="12" panose-1="2 14 5 2 3 3 3 2 2 4" units-per-em="1000" underline-position="-64.94141" underline-thickness="9.765625" slope="0" x-height="463.8672" cap-height="638.6719" ascent="952.1484" descent="-268.5547" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Candara"/>
+ </font-face-src>
+ </font-face>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledBall_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="#847b5a">
+ <g>
+ <circle cx="2.9999986" cy="0" r="2.9999973" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledBall_Marker_2" viewBox="-7 -4 8 8" markerWidth="8" markerHeight="8" color="#847b5a">
+ <g>
+ <circle cx="-2.9999986" cy="0" r="2.9999973" fill="currentColor" stroke="currentColor" stroke-width="1"/>
+ </g>
+ </marker>
+ <font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.68359" underline-thickness="49.316406" slope="0" x-height="522.9492" cap-height="717.28516" ascent="770.0195" descent="-229.98047" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Helvetica"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Helvetica Neue" font-size="12" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="bold">
+ <font-face-src>
+ <font-face-name name="HelveticaNeue-Bold"/>
+ </font-face-src>
+ </font-face>
+ <font-face font-family="Monaco" font-size="18" units-per-em="1000" underline-position="-37.597656" underline-thickness="75.68359" slope="0" x-height="560.5469" cap-height="780.2734" ascent="1e3" descent="-250" font-weight="500">
+ <font-face-src>
+ <font-face-name name="Monaco"/>
+ </font-face-src>
+ </font-face>
+ </defs>
+ <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1">
+ <title>nfvbench-trex-setup</title>
+ <g>
+ <title>Layer 1</title>
+ <rect x="228.375" y="310.268" width="182.09627" height="156.607" fill="#afb2e0"/>
+ <rect x="228.375" y="310.268" width="182.09627" height="156.607" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(233.375 310.268)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="32.039345" y="10" textLength="108.01758">Mercury Build Node</tspan>
+ </text>
+ <path d="M 253.06058 338.01395 L 317.68558 338.01395 C 323.7607 338.01395 328.68558 342.9388 328.68558 349.01395 L 328.68558 446.6201 C 328.68558 452.69523 323.7607 457.6201 317.68558 457.6201 L 253.06058 457.6201 C 246.98544 457.6201 242.06058 452.69523 242.06058 446.6201 L 242.06058 349.01395 C 242.06058 342.9388 246.98544 338.01395 253.06058 338.01395 Z" fill="#eaf2bf"/>
+ <path d="M 253.06058 338.01395 L 317.68558 338.01395 C 323.7607 338.01395 328.68558 342.9388 328.68558 349.01395 L 328.68558 446.6201 C 328.68558 452.69523 323.7607 457.6201 317.68558 457.6201 L 253.06058 457.6201 C 246.98544 457.6201 242.06058 452.69523 242.06058 446.6201 L 242.06058 349.01395 C 242.06058 342.9388 246.98544 338.01395 253.06058 338.01395 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="254.25" y="423.5208" width="62.40625" height="26.889958" fill="#e7d9fe"/>
+ <rect x="254.25" y="423.5208" width="62.40625" height="26.889958" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(254.25 429.9658)" fill="black">
+ <tspan font-family="Candara" font-size="12" font-weight="500" x="7.416992" y="11" textLength="47.572266">nfvbench</tspan>
+ </text>
+ <rect x="723.375" y="360.661" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="360.661" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 364.43103)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node 1</tspan>
+ </text>
+ <rect x="507.375" y="344.49533" width="73.125" height="45.305643" fill="#d1d9e6"/>
+ <rect x="507.375" y="344.49533" width="73.125" height="45.305643" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(512.375 360.23067)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="10.559082" y="10" textLength="42.006836">N9K TOR</tspan>
+ </text>
+ <rect x="260.14062" y="346.10956" width="50.625" height="43.69141" fill="#e7d9fe"/>
+ <rect x="260.14062" y="346.10956" width="50.625" height="43.69141" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(265.14062 361.0378)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="5.3100586" y="10" textLength="30.004883">T-rex</tspan>
+ </text>
+ <path d="M 285.06086 417.03247 C 284.49295 405.99827 285.21095 406.6487 285.4074 396.29963" marker-end="url(#FilledBall_Marker)" marker-start="url(#FilledBall_Marker_2)" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <rect x="348.065" y="346.10956" width="62.40625" height="43.69141" fill="#eae9dd"/>
+ <rect x="348.065" y="346.10956" width="62.40625" height="43.69141" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(353.065 347.20282)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="11.200684" y="10" textLength="30.004883">Intel</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="14.201172" y="23.83496" textLength="24.003906">X710</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="17.20166" y="37.66992" textLength="18.00293">NIC</tspan>
+ </text>
+ <path d="M 507.375 308.9902 L 499.375 308.9902 L 458.875 308.9902 L 458.875 360.67336 L 418.47127 360.67336 L 410.47127 360.67336" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="217.125" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="217.125" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 220.89502)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="288.893" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="288.893" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 292.66303)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <line x1="507.375" y1="374.6991" x2="410.47127" y2="375.23717" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="238.5" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="238.5" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 242.27002)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="259.875" width="137.25" height="21.375" fill="#fcc1b3"/>
+ <rect x="723.375" y="259.875" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 263.64502)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="27.61914" y="10" textLength="72.01172">Control Node</tspan>
+ </text>
+ <rect x="723.375" y="310.268" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="310.268" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 314.03803)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <rect x="723.375" y="331.643" width="137.25" height="21.375" fill="#bbcee3"/>
+ <rect x="723.375" y="331.643" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 335.41303)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="42.62158" y="10" textLength="42.006836">Storage</tspan>
+ </text>
+ <rect x="723.375" y="432.429" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="432.429" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 436.19904)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node i</tspan>
+ </text>
+ <rect x="507.375" y="286.33736" width="73.125" height="45.305643" fill="#d1d9e6"/>
+ <rect x="507.375" y="286.33736" width="73.125" height="45.305643" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(512.375 302.0727)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="10.559082" y="10" textLength="42.006836">N9K TOR</tspan>
+ </text>
+ <line x1="543.9375" y1="343.99533" x2="543.9375" y2="331.643" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width="6"/>
+ <path d="M 723.375 227.8125 L 716.875 227.8125 L 651.875 227.8125 L 651.875 308.9902 L 587 308.9902 L 580.5 308.9902" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <path d="M 723.375 231.375 L 716.875 231.375 L 657 231.375 L 657 353.018 L 657 359.5972 L 587 359.5972 L 580.5 359.5972" stroke="#2370ba" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"/>
+ <path d="M 723.375 443.1165 L 715.375 443.1165 L 672 443.1165 L 672 417.66667 L 672 374.6991 L 588.5 374.6991 L 580.5 374.6991" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <path d="M 723.375 435.9915 L 715.375 435.9915 L 679.3333 435.9915 L 679.3333 382.036 L 679.3333 315 L 580.5 315" stroke="#82645f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+ <rect x="723.375" y="482.822" width="137.25" height="21.375" fill="#dae68e"/>
+ <rect x="723.375" y="482.822" width="137.25" height="21.375" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <text transform="translate(728.375 486.59205)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="21.618164" y="10" textLength="84.01367">Compute node N</tspan>
+ </text>
+ <line x1="317.26562" y1="367.95526" x2="341.565" y2="367.95526" marker-end="url(#FilledBall_Marker)" marker-start="url(#FilledBall_Marker_2)" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
+ <path d="M 607.375 328.5 L 638 328.5 C 643.52285 328.5 648 332.97715 648 338.5 L 648 338.5 C 648 344.02285 643.52285 348.5 638 348.5 L 607.375 348.5 C 601.85215 348.5 597.375 344.02285 597.375 338.5 L 597.375 338.5 C 597.375 332.97715 601.85215 328.5 607.375 328.5 Z" fill="#e4e4e4"/>
+ <text transform="translate(607.375 331.5)" fill="#424242">
+ <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="#424242" x="3.977539" y="11" textLength="22.669922">vPC</tspan>
+ </text>
+ <path d="M 348.5 378.5715 L 355.5 378.5715 C 360.47056 378.5715 364.5 382.60094 364.5 387.5715 L 364.5 389.5715 C 364.5 394.54207 360.47056 398.5715 355.5 398.5715 L 348.5 398.5715 C 343.52944 398.5715 339.5 394.54207 339.5 389.5715 L 339.5 387.5715 C 339.5 382.60094 343.52944 378.5715 348.5 378.5715 Z" fill="#c75d5b"/>
+ <text transform="translate(348.5 380.89746)" fill="white">
+ <tspan font-family="Helvetica Neue" font-size="12" font-weight="bold" fill="white" x="0" y="12" textLength="6.672">1</tspan>
+ </text>
+ <path d="M 450 357.14815 L 457 357.14815 C 461.97056 357.14815 466 361.1776 466 366.14815 L 466 368.14815 C 466 373.1187 461.97056 377.14815 457 377.14815 L 450 377.14815 C 445.02944 377.14815 441 373.1187 441 368.14815 L 441 366.14815 C 441 361.1776 445.02944 357.14815 450 357.14815 Z" fill="#c75d5b"/>
+ <text transform="translate(450 359.4741)" fill="white">
+ <tspan font-family="Helvetica Neue" font-size="12" font-weight="bold" fill="white" x="0" y="12" textLength="6.672">2</tspan>
+ </text>
+ <path d="M 320.625 443.25 L 327.625 443.25 C 332.59556 443.25 336.625 447.27944 336.625 452.25 L 336.625 454.25 C 336.625 459.22056 332.59556 463.25 327.625 463.25 L 320.625 463.25 C 315.65444 463.25 311.625 459.22056 311.625 454.25 L 311.625 452.25 C 311.625 447.27944 315.65444 443.25 320.625 443.25 Z" fill="#c75d5b"/>
+ <text transform="translate(320.625 445.57596)" fill="white">
+ <tspan font-family="Helvetica Neue" font-size="12" font-weight="bold" fill="white" x="0" y="12" textLength="6.672">3</tspan>
+ </text>
+ <text transform="translate(451.625 452.9575)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x=".49121094" y="10" textLength="108.01758">add Intel X710 NIC</tspan>
+ </text>
+ <line x1="364.5" y1="388.5715" x2="446.625" y2="459.875" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(415.625 496.2075)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x=".48876953" y="10" textLength="138.02246">pull nfvbench container</tspan>
+ </text>
+ <line x1="336.625" y1="453.25" x2="410.625" y2="505.45833" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(492.75 406.65463)" fill="black">
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x=".4926758" y="10" textLength="90.01465">add a 10G cable</tspan>
+ <tspan font-family="Monaco" font-size="10" font-weight="500" x="12.494629" y="23.83496" textLength="66.01074">to each TOR</tspan>
+ </text>
+ <line x1="453.5" y1="377.14815" x2="492.75" y2="420.4896" stroke="#847b5a" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/>
+ <text transform="translate(295.375 222.99854)" fill="black">
+ <tspan font-family="Monaco" font-size="18" font-weight="500" x=".4824219" y="18" textLength="216.03516">nfvbench/T-rex setup</tspan>
+ </text>
+ </g>
+ </g>
+</svg>
diff --git a/docs/testing/user/userguide/index.rst b/docs/testing/user/userguide/index.rst
new file mode 100644
index 0000000..a7eb1e9
--- /dev/null
+++ b/docs/testing/user/userguide/index.rst
@@ -0,0 +1,30 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+
+.. NFVBench documentation master file, created by
+ sphinx-quickstart on Thu Sep 29 14:25:18 2016.
+
+
+NFVbench: A Network Performance Benchmarking Tool for NFVi Stacks
+*****************************************************************
+
+The NFVbench tool provides an automated way to measure the network performance for the most common data plane packet flows on any OpenStack system.
+It is designed to be easy to install and easy to use by non experts (no need to be an expert in traffic generators and data plane performance testing).
+
+
+Table of Content
+----------------
+.. toctree::
+ :maxdepth: 2
+
+ readme
+ installation
+ examples
+ advanced
+ server
+ faq
+
+
+
diff --git a/docs/testing/user/userguide/installation.rst b/docs/testing/user/userguide/installation.rst
new file mode 100644
index 0000000..8a0511a
--- /dev/null
+++ b/docs/testing/user/userguide/installation.rst
@@ -0,0 +1,14 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+===================================
+Installation and Quick Start Guides
+===================================
+
+.. toctree::
+ :maxdepth: 2
+
+ hw_requirements
+ quickstart_docker
+
diff --git a/docs/testing/user/userguide/quickstart_docker.rst b/docs/testing/user/userguide/quickstart_docker.rst
new file mode 100644
index 0000000..2c9f762
--- /dev/null
+++ b/docs/testing/user/userguide/quickstart_docker.rst
@@ -0,0 +1,224 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+===========================================
+NFVbench Installation and Quick Start Guide
+===========================================
+
+.. _docker_installation:
+
+Make sure you satisfy the `hardware and software requirements <requirements>` before you start .
+
+
+1. Container installation
+-------------------------
+
+To pull the latest NFVbench container image:
+
+.. code-block:: bash
+
+ docker pull opnfv/nfvbench/nfvbench
+
+2. Docker Container configuration
+---------------------------------
+
+The NFVbench container requires the following Docker options to operate properly.
+
++------------------------------------------------------+------------------------------------------------------+
+| Docker options | Description |
++======================================================+======================================================+
+| -v /lib/modules/$(uname -r):/lib/modules/$(uname -r) | needed by kernel modules in the container |
++------------------------------------------------------+------------------------------------------------------+
+| -v /dev:/dev | needed by kernel modules in the container |
++------------------------------------------------------+------------------------------------------------------+
+| -v $PWD:/tmp/nfvbench | optional but recommended to pass files between the |
+| | host and the docker space (see examples below) |
+| | Here we map the current directory on the host to the |
+| | /tmp/nfvbench director in the container but any |
+| | other similar mapping can work as well |
++------------------------------------------------------+------------------------------------------------------+
+| --net=host | (optional) needed if you run the NFVbench REST |
+| | server in the container (or use any appropriate |
+| | docker network mode other than "host") |
++------------------------------------------------------+------------------------------------------------------+
+| --privilege | (optional) required if SELinux is enabled on the host|
++------------------------------------------------------+------------------------------------------------------+
+
+It can be convenient to write a shell script (or an alias) to automatically insert the necessary options.
+
+3. Start the Docker container
+-----------------------------
+As for any Docker container, you can execute NFVbench measurement sessions using a temporary container ("docker run" - which exits after each NFVbench run)
+or you can decide to run the NFVbench container in the background then execute one or more NFVbench measurement sessions on that container ("docker exec").
+
+The former approach is simpler to manage (since each container is started and terminated after each command) but incurs a small delay at start time (several seconds).
+The second approach is more responsive as the delay is only incurred once when starting the container.
+
+We will take the second approach and start the NFVbench container in detached mode with the name "nfvbench" (this works with bash, prefix with "sudo" if you do not use the root login)
+
+.. code-block:: bash
+
+ docker run --detach --net=host --privileged -v $PWD:/tmp/nfvbench -v /dev:/dev -v /lib/modules/$(uname -r):/lib/modules/$(uname -r) --name nfvbench opnfv/nfvbench tail -f /dev/null
+
+The tail command simply prevents the container from exiting.
+
+The create an alias to make it easy to execute nfvbench commands directly from the host shell prompt:
+
+.. code-block:: bash
+
+ alias nfvbench='docker exec -it nfvbench nfvbench'
+
+The next to last "nfvbench" refers to the name of the container while the last "nfvbench" refers to the NFVbench binary that is available to run in the container.
+
+To verify it is working:
+
+.. code-block:: bash
+
+ nfvbench --version
+ nfvbench --help
+
+
+4. NFVbench configuration
+-------------------------
+
+Create a new file containing the minimal configuration for NFVbench, we can call it any name, for example "my_nfvbench.cfg" and paste the following yaml template in the file:
+
+.. code-block:: bash
+
+ openrc_file:
+ traffic_generator:
+ generator_profile:
+ - name: trex-local
+ tool: TRex
+ ip: 127.0.0.1
+ cores: 3
+ interfaces:
+ - port: 0
+ switch_port:
+ pci:
+ - port: 1
+ switch_port:
+ pci:
+ intf_speed: 10Gbps
+
+NFVbench requires an ``openrc`` file to connect to OpenStack using the OpenStack API. This file can be downloaded from the OpenStack Horizon dashboard (refer to the OpenStack documentation on how to
+retrieve the openrc file). The file pathname in the container must be stored in the "openrc_file" property. If it is stored on the host in the current directory, its full pathname must start with /tmp/nfvbench (since the current directory is mapped to /tmp/nfvbench in the container).
+
+The required configuration is the PCI address of the 2 physical interfaces that will be used by the traffic generator. The PCI address can be obtained for example by using the "lspci" Linux command. For example:
+
+.. code-block:: bash
+
+ [root@sjc04-pod6-build ~]# lspci | grep 710
+ 0a:00.0 Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (rev 01)
+ 0a:00.1 Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (rev 01)
+ 0a:00.2 Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (rev 01)
+ 0a:00.3 Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (rev 01)
+
+
+Example of edited configuration with an OpenStack RC file stored in the current directory with the "openrc" name, and
+PCI addresses "0a:00.0" and "0a:00.1" (first 2 ports of the quad port NIC):
+
+.. code-block:: bash
+
+ openrc_file: /tmp/nfvbench/openrc
+ traffic_generator:
+ generator_profile:
+ - name: trex-local
+ tool: TRex
+ ip: 127.0.0.1
+ cores: 3
+ interfaces:
+ - port: 0
+ switch_port:
+ pci: 0a:00.0
+ - port: 1
+ switch_port:
+ pci: 0a:00.1
+ intf_speed: 10Gbps
+
+Alternatively, the full template with comments can be obtained using the --show-default-config option in yaml format:
+
+.. code-block:: bash
+
+ nfvbench --show-default-config > my_nfvbench.cfg
+
+Edit the nfvbench.cfg file to only keep those properties that need to be modified (preserving the nesting)
+
+
+5. Upload the NFVbench loopback VM image to OpenStack
+-----------------------------------------------------
+[TBP URL to NFVbench VM image in the OPNFV artifact repository]
+
+
+6. Run NFVbench
+---------------
+
+To do a single run at 5000pps bi-directional using the PVP packet path:
+
+.. code-block:: bash
+
+ nfvbench -c /tmp/nfvbench/my_nfvbench.cfg --rate 5kpps
+
+NFVbench options used:
+
+* ``-c /tmp/nfvbench/my_nfvbench.cfg`` : specify the config file to use (this must reflect the file path from inside the container)
+* ``--rate 5kpps`` : specify rate of packets for test using the kpps unit (thousands of packets per second)
+
+This should produce a result similar to this (a simple run with the above options should take less than 5 minutes):
+
+.. code-block:: none
+
+ ========== nfvbench Summary ==========
+ Date: 2016-10-05 21:43:30
+ nfvbench version 0.0.1.dev128
+ Mercury version: 5002
+ Benchmarks:
+ > Networks:
+ > N9K version: {'10.28.108.249': {'BIOS': '07.34', 'NXOS': '7.0(3)I2(2b)'}, '10.28.108.248': {'BIOS': '07.34', 'NXOS': '7.0(3)I2(2b)'}}
+ Traffic generator profile: trex-c45
+ Traffic generator tool: TRex
+ Traffic generator API version: {u'build_date': u'Aug 24 2016', u'version': u'v2.08', u'built_by': u'hhaim', u'build_time': u'16:32:13'}
+ Flows:
+ > PVP:
+ VPP version: {u'sjc04-pod3-compute-6': 'v16.06-rc1~27-gd175728'}
+ > Bidirectional: False
+ Profile: traffic_profile_64B
+
+ +-----------------+-------------+----------------------+----------------------+----------------------+
+ | L2 Frame Size | Drop Rate | Avg Latency (usec) | Min Latency (usec) | Max Latency (usec) |
+ +=================+=============+======================+======================+======================+
+ | 64 | 0.0000% | 22.1885 | 10 | 503 |
+ +-----------------+-------------+----------------------+----------------------+----------------------+
+
+
+ > L2 frame size: 64
+ Flow analysis duration: 70.0843 seconds
+
+ Run Config:
+
+ +-------------+------------------+--------------+-----------+
+ | Direction | Duration (sec) | Rate | Rate |
+ +=============+==================+==============+===========+
+ | Forward | 60 | 1.0080 Mbps | 1,500 pps |
+ +-------------+------------------+--------------+-----------+
+ | Reverse | 60 | 672.0000 bps | 1 pps |
+ +-------------+------------------+--------------+-----------+
+
+ +----------------------+----------+-----------------+---------------+---------------+-----------------+---------------+---------------+
+ | Interface | Device | Packets (fwd) | Drops (fwd) | Drop% (fwd) | Packets (rev) | Drops (rev) | Drop% (rev) |
+ +======================+==========+=================+===============+===============+=================+===============+===============+
+ | traffic-generator | trex | 90,063 | | | 61 | 0 | - |
+ +----------------------+----------+-----------------+---------------+---------------+-----------------+---------------+---------------+
+ | traffic-generator | trex | 90,063 | 0 | - | 61 | | |
+ +----------------------+----------+-----------------+---------------+---------------+-----------------+---------------+---------------+
+
+7. Terminating the NFVbench container
+-------------------------------------
+When no longer needed, the container can be terminated using the usual docker commands:
+
+.. code-block:: bash
+
+ docker kill nfvbench
+ docker rm nfvbench
+
diff --git a/docs/testing/user/userguide/readme.rst b/docs/testing/user/userguide/readme.rst
new file mode 100644
index 0000000..17ce889
--- /dev/null
+++ b/docs/testing/user/userguide/readme.rst
@@ -0,0 +1,163 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+Features
+********
+
+Data Plane Performance Measurement Features
+-------------------------------------------
+
+NFVbench supports the following main measurement capabilities:
+
+- supports 2 measurement modes:
+ - *fixed rate* mode to generate traffic at a fixed rate for a fixed duration
+ - NDR (No Drop Rate) and PDR (Partial Drop Rate) measurement mode
+- configurable frame sizes (any list of fixed sizes or 'IMIX')
+- built-in packet paths
+- built-in loopback VNFs based on fast L2 or L3 forwarders running in VMs
+- configurable number of flows and service chains
+- configurable traffic direction (single or bi-directional)
+
+
+NDR is the highest throughput achieved without dropping packets.
+PDR is the highest throughput achieved without dropping more than a pre-set limit (called PDR threshold or allowance, expressed in %).
+
+Results of each run include the following data:
+
+- Aggregated achieved throughput in bps
+- Aggregated achieved packet rate in pps (or fps)
+- Actual drop rate in %
+- Latency in usec (min, max, average in the current version)
+
+Built-in OpenStack support
+--------------------------
+NFVbench can stage OpenStack resources to build 1 or more service chains using direct OpenStack APIs. Each service chain is composed of:
+
+- 1 or 2 loopback VM instances per service chain
+- 2 Neutron networks per loopback VM
+
+OpenStack resources are staged before traffic is measured using OpenStack APIs (Nova and Neutron) then disposed after completion of measurements.
+
+The loopback VM flavor to use can be configured in the NFVbench configuration file.
+
+Note that NFVbench does not use OpenStack Heat nor any higher level service (VNFM or NFVO) to create the service chains because its
+main purpose is to measure the performance of the NFVi infrastructure which is mainly focused on L2 forwarding performance.
+
+External Chains
+---------------
+NFVbench also supports settings that involve externally staged packet paths with or without OpenStack:
+
+- run benchmarks on existing service chains at the L3 level that are staged externally by any other tool (e.g. any VNF capable of L3 routing)
+- run benchmarks on existing L2 chains that are configured externally (e.g. pure L2 forwarder such as DPDK testpmd)
+
+
+Traffic Generation
+------------------
+
+NFVbench currently integrates with the open source TRex traffic generator:
+
+- `TRex <https://trex-tgn.cisco.com>`_ (pre-built into the NFVbench container)
+
+
+Supported Packet Paths
+----------------------
+Packet paths describe where packets are flowing in the NFVi platform. The most commonly used paths are identified by 3 or 4 letter abbreviations.
+A packet path can generally describe the flow of packets associated to one or more service chains, with each service chain composed of 1 or more VNFs.
+
+The following packet paths are currently supported by NFVbench:
+
+- PVP (Physical interface to VM to Physical interface)
+- PVVP (Physical interface to VM to VM to Physical interface)
+- N*PVP (N concurrent PVP packet paths)
+- N*PVVP (N concurrent PVVP packet paths)
+
+The traffic is made of 1 or more flows of L3 frames (UDP packets) with different payload sizes. Each flow is identified by a unique source and destination MAC/IP tuple.
+
+
+Loopback VM
+^^^^^^^^^^^
+
+NFVbench provides a loopback VM image that runs CentOS with 2 pre-installed forwarders:
+
+- DPDK testpmd configured to do L2 cross connect between 2 virtual interfaces
+- FD.io VPP configured to perform L3 routing between 2 virtual interfaces
+
+Frames are just forwarded from one interface to the other.
+In the case of testpmd, the source and destination MAC are rewritten, which corresponds to the mac forwarding mode (--forward-mode=mac).
+In the case of VPP, VPP will act as a real L3 router, and the packets are routed from one port to the other using static routes.
+
+Which forwarder and what Nova flavor to use can be selected in the NFVbench configuration. Be default the DPDK testpmd forwarder is used with 2 vCPU per VM.
+The configuration of these forwarders (such as MAC rewrite configuration or static route configuration) is managed by NFVbench.
+
+
+PVP Packet Path
+^^^^^^^^^^^^^^^
+
+This packet path represents a single service chain with 1 loopback VNF and 2 Neutron networks:
+
+.. image:: images/nfvbench-pvp.svg
+
+
+PVVP Packet Path
+^^^^^^^^^^^^^^^^
+
+This packet path represents a single service chain with 2 loopback VNFs in sequence and 3 Neutron networks.
+The 2 VNFs can run on the same compute node (PVVP intra-node):
+
+.. image:: images/nfvbench-pvvp-intra.svg
+
+or on different compute nodes (PVVP inter-node) based on a configuration option:
+
+.. image:: images/nfvbench-pvvp-inter.svg
+
+
+
+Multi-Chaining (N*PVP or N*PVVP)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Multiple service chains can be setup by NFVbench without any limit on the concurrency (other than limits imposed by available resources on compute nodes).
+In the case of multiple service chains, NFVbench will instruct the traffic generator to use multiple L3 packet streams (frames directed to each path will have a unique destination MAC address).
+
+Example of multi-chaining with 2 concurrent PVP service chains:
+
+.. image:: images/nfvbench-npvp.svg
+
+This innovative feature will allow to measure easily the performance of a fully loaded compute node running multiple service chains.
+
+Multi-chaining is currently limited to 1 compute node (PVP or PVVP intra-node) or 2 compute nodes (for PVVP inter-node).
+The 2 edge interfaces for all service chains will share the same 2 networks.
+
+
+Other Misc Packet Paths
+^^^^^^^^^^^^^^^^^^^^^^^
+
+P2P (Physical interface to Physical interface - no VM) can be supported using the external chain/L2 forwarding mode.
+
+V2V (VM to VM) is not supported but PVVP provides a more complete (and mroe realistic) alternative.
+
+
+Supported Neutron Network Plugins and vswitches
+-----------------------------------------------
+
+Any Virtual Switch, Any Encapsulation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+NFVbench is agnostic of the virtual switch implementation and has been tested with the following virtual switches:
+
+- ML2/VPP/VLAN (networking-vpp)
+- OVS/VLAN and OVS-DPDK/VLAN
+- ML2/ODL/VPP (OPNFV Fast Data Stack)
+
+SR-IOV
+^^^^^^
+
+By default, service chains will be based on virtual switch interfaces.
+
+NFVbench provides an option to select SR-IOV based virtual interfaces instead (thus bypassing any virtual switch) for those OpenStack system that include and support SR-IOV capable NICs on compute nodes.
+
+
+
+
+
+
diff --git a/docs/testing/user/userguide/server.rst b/docs/testing/user/userguide/server.rst
new file mode 100644
index 0000000..4495e19
--- /dev/null
+++ b/docs/testing/user/userguide/server.rst
@@ -0,0 +1,445 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. (c) Cisco Systems, Inc
+
+NFVbench Server mode and NFVbench client API
+============================================
+
+NFVbench can run as an HTTP server to:
+
+- optionally provide access to any arbitrary HTLM files (HTTP server function) - this is optional
+- service fully parameterized aynchronous run requests using the HTTP protocol (REST/json with polling)
+- service fully parameterized run requests with interval stats reporting using the WebSocket/SocketIO protocol.
+
+Start the NFVbench server
+-------------------------
+To run in server mode, simply use the --server <http_root_path> and optionally the listen address to use (--host <ip>, default is 0.0.0.0) and listening port to use (--port <port>, default is 7555).
+
+
+If HTTP files are to be serviced, they must be stored right under the http root path.
+This root path must contain a static folder to hold static files (css, js) and a templates folder with at least an index.html file to hold the template of the index.html file to be used.
+This mode is convenient when you do not already have a WEB server hosting the UI front end.
+If HTTP files servicing is not needed (REST only or WebSocket/SocketIO mode), the root path can point to any dummy folder.
+
+Once started, the NFVbench server will be ready to service HTTP or WebSocket/SocketIO requests at the advertised URL.
+
+Example of NFVbench server start in a container:
+
+.. code-block:: bash
+
+ # get to the container shell (assume the container name is "nfvbench")
+ docker exec -it nfvbench bash
+ # from the container shell start the NFVbench server in the background
+ nfvbench -c /tmp/nfvbench/nfvbench.cfg --server /tmp &
+ # exit container
+ exit
+
+
+
+HTTP Interface
+--------------
+
+<http-url>/echo (GET)
+^^^^^^^^^^^^^^^^^^^^^
+
+This request simply returns whatever content is sent in the body of the request (only used for testing)
+
+<http-url>/start_run (POST)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This request will initiate a new NFVbench run asynchornously and can optionally pass the NFVbench configuration to run in the body (in JSON format).
+See "NFVbench configuration JSON parameter" below for details on how to format this parameter.
+
+The request returns immediately with a json content indicating if there was an error (status=ERROR) or if the request was submitted successfully (status=PENDING). Example of return when the submission is successful:
+
+.. code-block:: bash
+
+ {
+ "error_message": "nfvbench run still pending",
+ "status": "PENDING"
+ }
+
+<http-url>/status (GET)
+^^^^^^^^^^^^^^^^^^^^^^^
+
+This request fetches the status of an asynchronous run. It will return in json format:
+
+- a request pending reply (if the run is still not completed)
+- an error reply if there is no run pending
+- or the complete result of the run
+
+The client can keep polling until the run completes.
+
+Example of return when the run is still pending:
+
+.. code-block:: bash
+
+ {
+ "error_message": "nfvbench run still pending",
+ "status": "PENDING"
+ }
+
+Example of return when the run completes:
+
+.. code-block:: bash
+
+ {
+ "result": {...}
+ "status": "OK"
+ }
+
+
+
+WebSocket/SocketIO events
+-------------------------
+
+List of SocketIO events supported:
+
+Client to Server
+^^^^^^^^^^^^^^^^
+
+start_run:
+
+ sent by client to start a new run with the configuration passed in argument (JSON).
+ The configuration can be any valid NFVbench configuration passed as a JSON document (see "NFVbench configuration JSON parameter" below)
+
+Server to Client
+^^^^^^^^^^^^^^^^
+
+run_interval_stats:
+
+ sent by server to report statistics during a run
+ the message contains the statistics {'time_ms': time_ms, 'tx_pps': tx_pps, 'rx_pps': rx_pps, 'drop_pct': drop_pct}
+
+ndr_found:
+
+ (during NDR-PDR search)
+ sent by server when the NDR rate is found
+ the message contains the NDR value {'rate_pps': ndr_pps}
+
+ndr_found:
+
+ (during NDR-PDR search)
+ sent by server when the PDR rate is found
+ the message contains the PDR value {'rate_pps': pdr_pps}
+
+
+run_end:
+
+ sent by server to report the end of a run
+ the message contains the complete results in JSON format
+
+NFVbench configuration JSON parameter
+-------------------------------------
+The NFVbench configuration describes the parameters of an NFVbench run and can be passed to the NFVbench server as a JSON document.
+
+Default configuration
+^^^^^^^^^^^^^^^^^^^^^
+
+The simplest JSON document is the empty dictionary "{}" which indicates to use the default NFVbench configuration:
+
+- PVP
+- NDR-PDR measurement
+- 64 byte packets
+- 1 flow per direction
+
+The entire default configuration can be viewed using the --show-json-config option on the cli:
+
+.. code-block:: bash
+
+ # nfvbench --show-json-config
+ {
+ "availability_zone": null,
+ "compute_node_user": "root",
+ "compute_nodes": null,
+ "debug": false,
+ "duration_sec": 60,
+ "flavor": {
+ "disk": 0,
+ "extra_specs": {
+ "hw:cpu_policy": "dedicated",
+ "hw:mem_page_size": 2048
+ },
+ "ram": 8192,
+ "vcpus": 2
+ },
+ "flavor_type": "nfv.medium",
+ "flow_count": 1,
+ "generic_poll_sec": 2,
+ "generic_retry_count": 100,
+ "image_name": "nfvbenchvm",
+ "inter_node": false,
+ "internal_networks": {
+ "left": {
+ "name": "nfvbench-net0",
+ "subnet": "nfvbench-subnet0",
+ "cidr": "192.168.1.0/24",
+ },
+ "right": {
+ "name": "nfvbench-net1",
+ "subnet": "nfvbench-subnet1",
+ "cidr": "192.168.2.0/24",
+ },
+ "middle": {
+ "name": "nfvbench-net2",
+ "subnet": "nfvbench-subnet2",
+ "cidr": "192.168.3.0/24",
+ }
+ },
+ "interval_sec": 10,
+ "json": null,
+ "loop_vm_name": "nfvbench-loop-vm",
+ "measurement": {
+ "NDR": 0.001,
+ "PDR": 0.1,
+ "load_epsilon": 0.1
+ },
+ "name": "(built-in default config)",
+ "no_cleanup": false,
+ "no_int_config": false,
+ "no_reset": false,
+ "no_tor_access": false,
+ "no_traffic": false,
+ "no_vswitch_access": false,
+ "openrc_file": "/tmp/nfvbench/openstack/openrc",
+ "openstack_defaults": "/tmp/nfvbench/openstack/defaults.yaml",
+ "openstack_setup": "/tmp/nfvbench/openstack/setup_data.yaml",
+ "rate": "ndr_pdr",
+ "service_chain": "PVP",
+ "service_chain_count": 1,
+ "sriov": false,
+ "std_json": null,
+ "tor": {
+ "switches": [
+ {
+ "host": "172.26.233.12",
+ "password": "lab",
+ "port": 22,
+ "username": "admin"
+ }
+ ],
+ "type": "N9K"
+ },
+ "traffic": {
+ "bidirectional": true,
+ "profile": "traffic_profile_64B"
+ },
+ "traffic_generator": {
+ "default_profile": "trex-local",
+ "gateway_ip_addrs": [
+ "1.1.0.2",
+ "2.2.0.2"
+ ],
+ "gateway_ip_addrs_step": "0.0.0.1",
+ "generator_profile": [
+ {
+ "cores": 3,
+ "interfaces": [
+ {
+ "pci": "0a:00.0",
+ "port": 0,
+ "switch_port": "Ethernet1/33",
+ "vlan": null
+ },
+ {
+ "pci": "0a:00.1",
+ "port": 1,
+ "switch_port": "Ethernet1/34",
+ "vlan": null
+ }
+ ],
+ "intf_speed": "10Gbps",
+ "ip": "127.0.0.1",
+ "name": "trex-local",
+ "tool": "TRex"
+ }
+ ],
+ "host_name": "nfvbench_tg",
+ "ip_addrs": [
+ "10.0.0.0/8",
+ "20.0.0.0/8"
+ ],
+ "ip_addrs_step": "0.0.0.1",
+ "mac_addrs": [
+ "00:10:94:00:0A:00",
+ "00:11:94:00:0A:00"
+ ],
+ "step_mac": null,
+ "tg_gateway_ip_addrs": [
+ "1.1.0.100",
+ "2.2.0.100"
+ ],
+ "tg_gateway_ip_addrs_step": "0.0.0.1"
+ },
+ "traffic_profile": [
+ {
+ "l2frame_size": [
+ "64"
+ ],
+ "name": "traffic_profile_64B"
+ },
+ {
+ "l2frame_size": [
+ "IMIX"
+ ],
+ "name": "traffic_profile_IMIX"
+ },
+ {
+ "l2frame_size": [
+ "1518"
+ ],
+ "name": "traffic_profile_1518B"
+ },
+ {
+ "l2frame_size": [
+ "64",
+ "IMIX",
+ "1518"
+ ],
+ "name": "traffic_profile_3sizes"
+ }
+ ],
+ "unidir_reverse_traffic_pps": 1,
+ "vlan_tagging": true,
+ "vm_image_file": "file://172.29.172.152/downloads/nfvbench/nfvbenchvm-latest.qcow2",
+ "vts_ncs": {
+ "host": null,
+ "password": "secret",
+ "port": 22,
+ "username": "admin"
+ }
+ }
+
+
+Common examples of JSON configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Use the default configuration but use 10000 flows per direction (instead of 1):
+
+.. code-block:: bash
+
+ { "flow_count": 10000 }
+
+
+Use default confguration but with 10000 flows, "EXT" chain and IMIX packet size:
+
+.. code-block:: bash
+
+ {
+ "flow_count": 10000,
+ "service_chain": "EXT",
+ "traffic": {
+ "profile": "traffic_profile_IMIX"
+ },
+ }
+
+A short run of 5 seconds at a fixed rate of 1Mpps (and everything else same as the default configuration):
+
+.. code-block:: bash
+
+ {
+ "duration": 5,
+ "rate": "1Mpps"
+ }
+
+Example of interaction with the NFVbench server using HTTP and curl
+-------------------------------------------------------------------
+HTTP requests can be sent directly to the NFVbench server from CLI using curl from any host that can connect to the server (here we run it from the local host).
+
+This is a POST request to start a run using the default NFVbench configuration but with traffic generation disabled ("no_traffic" property is set to true):
+
+.. code-block:: bash
+
+ [root@sjc04-pod3-mgmt ~]# curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"no_traffic":true}' http://127.0.0.1:7555/start_run
+ {
+ "error_message": "nfvbench run still pending",
+ "status": "PENDING"
+ }
+ [root@sjc04-pod3-mgmt ~]#
+
+This request will return immediately with status set to "PENDING" if the request was started successfully.
+
+The status can be polled until the run completes. Here the poll returns a "PENDING" status, indicating the run is still not completed:
+
+.. code-block:: bash
+
+ [root@sjc04-pod3-mgmt ~]# curl -G http://127.0.0.1:7555/status
+ {
+ "error_message": "nfvbench run still pending",
+ "status": "PENDING"
+ }
+ [root@sjc04-pod3-mgmt ~]#
+
+Finally, the status request returns a "OK" status along with the full results (truncated here):
+
+.. code-block:: bash
+
+ [root@sjc04-pod3-mgmt ~]# curl -G http://127.0.0.1:7555/status
+ {
+ "result": {
+ "benchmarks": {
+ "network": {
+ "service_chain": {
+ "PVP": {
+ "result": {
+ "bidirectional": true,
+ "compute_nodes": {
+ "nova:sjc04-pod3-compute-4": {
+ "bios_settings": {
+ "Adjacent Cache Line Prefetcher": "Disabled",
+ "All Onboard LOM Ports": "Enabled",
+ "All PCIe Slots OptionROM": "Enabled",
+ "Altitude": "300 M",
+ ...
+
+ "date": "2017-03-31 22:15:41",
+ "nfvbench_version": "0.3.5",
+ "openstack_spec": {
+ "encaps": "VxLAN",
+ "vswitch": "VTS"
+ }
+ },
+ "status": "OK"
+ }
+ [root@sjc04-pod3-mgmt ~]#
+
+
+
+Example of interaction with the NFVbench server using a python CLI app (nfvbench_client)
+----------------------------------------------------------------------------------------
+The module client/client.py contains an example of python class that can be used to control the NFVbench server from a python app using HTTP or WebSocket/SocketIO.
+
+The module client/nfvbench_client.py has a simple main application to control the NFVbench server from CLI.
+The "nfvbench_client" wrapper script can be used to invoke the client front end (this wrapper is pre-installed in the NFVbench container)
+
+Example of invocation of the nfvbench_client front end, from the host (assume the name of the NFVbench container is "nfvbench"),
+use the default NFVbench configuration but do not generate traffic (no_traffic property set to true, the full json result is truncated here):
+
+.. code-block:: bash
+
+ [root@sjc04-pod3-mgmt ~]# docker exec -it nfvbench nfvbench_client -c '{"no_traffic":true}' http://127.0.0.1:7555
+ {u'status': u'PENDING', u'error_message': u'nfvbench run still pending'}
+ {u'status': u'PENDING', u'error_message': u'nfvbench run still pending'}
+ {u'status': u'PENDING', u'error_message': u'nfvbench run still pending'}
+
+ {u'status': u'OK', u'result': {u'date': u'2017-03-31 22:04:59', u'nfvbench_version': u'0.3.5',
+ u'config': {u'compute_nodes': None, u'compute_node_user': u'root', u'vts_ncs': {u'username': u'admin', u'host': None, u'password': u'secret', u'port': 22}, u'traffic_generator': {u'tg_gateway_ip_addrs': [u'1.1.0.100', u'2.2.0.100'], u'ip_addrs_step': u'0.0.0.1', u'step_mac': None, u'generator_profile': [{u'intf_speed': u'10Gbps', u'interfaces': [{u'pci': u'0a:00.0', u'port': 0, u'vlan': 1998, u'switch_port': None},
+
+ ...
+
+ [root@sjc04-pod3-mgmt ~]#
+
+The http interface is used unless --use-socketio is defined.
+
+Example of invocation using Websocket/SocketIO, execute NFVbench using the default configuration but with a duration of 5 seconds and a fixed rate run of 5kpps.
+
+.. code-block:: bash
+
+ [root@sjc04-pod3-mgmt ~]# docker exec -it nfvbench nfvbench_client -c '{"duration":5,"rate":"5kpps"}' --use-socketio http://127.0.0.1:7555 >results.json
+
+
+
+
+
+
+
diff --git a/nfvbench/__init__.py b/nfvbench/__init__.py
new file mode 100644
index 0000000..6e88400
--- /dev/null
+++ b/nfvbench/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import pbr.version
+
+__version__ = pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
diff --git a/nfvbench/cfg.default.yaml b/nfvbench/cfg.default.yaml
new file mode 100644
index 0000000..795ed5d
--- /dev/null
+++ b/nfvbench/cfg.default.yaml
@@ -0,0 +1,337 @@
+#
+# NFVbench default configuration file
+#
+# This configuration file is ALWAYS loaded by NFVbench and should never be modified by users.
+# To specify your own property values, always define them in a separate config file
+# and pass that file to the script using -c or --config <file>
+# Property values in that config file will override the default values in the current file
+#
+---
+# IMPORTANT CUSTOMIZATION NOTES
+# There are roughly 2 types of NFVbench config based on the OpenStack encaps used:
+# - VLAN (OVS, OVS-DPDK, ML2/VPP)
+# Many of the fields to customize are relevant to only 1 of the 2 encaps
+# These will be clearly labeled "VxLAN only" or "VLAN only"
+# Fields that are not applicable will not be used by NFVbench and can be left empty
+#
+# All fields are applicable to all encaps/traffic generators unless explicitly marked otherwise.
+# Fields that can be over-ridden at the command line are marked with the corresponding
+# option, e.g. "--interval"
+
+
+# Name of the image to use for launching the loopback VMs. This name must be
+# the exact same name used in OpenStack (as shown from 'nova image-list')
+# Can be overridden by --image or -i
+image_name: 'nfvbenchvm'
+# Forwarder to use in nfvbenchvm image. Available options: ['vpp', 'testpmd']
+vm_forwarder: testpmd
+
+# NFVbench can automatically upload a VM image if the image named by
+# image_name is missing, for that you need to specify a file location where
+# the image can be retrieved
+#
+# To upload the image as a file, download it to preferred location
+# and prepend it with file:// like in this example:
+# file://<location of the image>
+# NFVbench (the image must have the same name as defined in image_name above).
+vm_image_file:
+
+# Name of the flavor to use for the loopback VMs
+#
+# If the provided name is an exact match to a flavor name known by OpenStack
+# (as shown from 'nova flavor-list'), that flavor will be reused.
+# Otherwise, a new flavor will be created with attributes listed below.
+flavor_type: 'nfvbench.medium'
+
+# Custom flavor attributes
+flavor:
+ # Number of vCPUs for the flavor
+ vcpus: 2
+ # Memory for the flavor in MB
+ ram: 8192
+ # Size of local disk in GB
+ disk: 0
+ # metadata are supported and can be added if needed, optional
+ # note that if your openstack does not have NUMA optimization
+ # (cpu pinning and huge pages)
+ # you must comment out extra_specs completely otherwise
+ # loopback VM creation will fail
+ extra_specs:
+ "hw:cpu_policy": dedicated
+ "hw:mem_page_size": large
+
+# Name of the availability zone to use for the test VMs
+# Must be one of the zones listed by 'nova availability-zone-list'
+# If the selected zone contains only 1 compute node and PVVP inter-node flow is selected,
+# application will use intra-node PVVP flow.
+# List of compute nodes can be specified, must be in given availability zone if not empty
+#availability_zone: 'nova'
+availability_zone:
+compute_nodes:
+
+
+# Credentials for SSH connection to TOR switches.
+tor:
+ # Leave type empty or switch list empty to skip TOR switches configuration.
+ # Preferably use 'no_tor_access' to achieve the same behavior.
+ # (skipping TOR config will require the user to pre-stitch the traffic generator interfaces
+ # to the service chain under test, needed only if configured in access mode)
+ type:
+ # Switches are only needed if type is not empty.
+ # You can configure 0, 1 or 2 switches
+ # no switch: in this case NFVbench will not attempt to ssh to the switch
+ # and stitching of traffic must be done externally
+ # 1 switch: this assumes that both traffic generator interfaces are wired to the same switch
+ # 2 switches: this is the recommended setting wuth redundant switches, in this case each
+ # traffic generator interface must be wired to a different switch
+ switches:
+ - host:
+ username:
+ password:
+ port:
+
+# Skip TOR switch configuration and retrieving of stats
+# Can be overriden by --no-tor-access
+no_tor_access: false
+
+# Skip vswitch configuration and retrieving of stats
+# Can be overriden by --no-vswitch-access
+no_vswitch_access: false
+
+# Type of service chain to run, possible options are PVP, PVVP and EXT
+# PVP - port to VM to port
+# PVVP - port to VM to VM to port
+# EXT - external chain used only for running traffic and checking traffic generator counters,
+# all other parts of chain must be configured manually
+# Can be overriden by --service-chain
+service_chain: 'PVP'
+
+# Total number of service chains, every chain has own traffic stream
+# Can be overriden by --service-chain-count
+service_chain_count: 1
+
+# Total number of traffic flows for all chains and directions generated by the traffic generator.
+# Minimum is '2 * service_chain_count', it is automatically adjusted if too small
+# value was configured. Must be even.
+# Every flow has packets with different IPs in headers
+# Can be overriden by --flow-count
+flow_count: 2
+
+# Used by PVVP chain to spawn VMs on different compute nodes
+# Can be overriden by --inter-node
+inter_node: false
+
+# set to true if service chains should use SRIOV
+# This requires SRIOV to be available on compute nodes
+sriov: false
+
+# Skip interfaces config on EXT service chain
+# Can be overriden by --no-int-config
+no_int_config: false
+
+# Resources created by NFVbench will not be removed
+# Can be overriden by --no-cleanup
+no_cleanup: false
+
+# Configuration for traffic generator
+traffic_generator:
+ # Name of the traffic generator, only for informational purposes
+ host_name: 'nfvbench_tg'
+ # this is the default traffic generator profile to use
+ # the name must be defined under generator_profile
+ # you can override the traffic generator to use using the
+ # -g or --traffic-gen option at the command line
+ default_profile: trex-local
+
+ # IP addresses for L3 traffic.
+ # All of the IPs are used as base for IP sequence computed based on chain or flow count.
+ #
+ # `ip_addrs` base IPs used as src and dst in packet header, quantity depends on flow count
+ # `ip_addrs_step`: step for generating IP sequence. Use "random" for random patterns, default is 0.0.0.1.
+ # `tg_gateway_ip_addrs` base IPs for traffic generator ports, quantity depends on chain count
+ # `tg_gateway_ip_addrs__step`: step for generating traffic generator gateway sequences. default is 0.0.0.1
+ # `gateway_ip_addrs`: base IPs of router gateways on both networks, quantity depends on chain count
+ # `gateway_ip_addrs_step`: step for generating router gateway sequences. default is 0.0.0.1
+ ip_addrs: ['10.0.0.0/8', '20.0.0.0/8']
+ ip_addrs_step: 0.0.0.1
+ tg_gateway_ip_addrs: ['1.1.0.100', '2.2.0.100']
+ tg_gateway_ip_addrs_step: 0.0.0.1
+ gateway_ip_addrs: ['1.1.0.2', '2.2.0.2']
+ gateway_ip_addrs_step: 0.0.0.1
+
+ # Traffic Generator Profiles
+ # In case you have multiple testbeds or traffic generators,
+ # you can define one traffic generator profile per testbed/traffic generator.
+ #
+ # Generator profiles are listed in the following format:
+ # `name`: Traffic generator profile name (use a unique name, no space or special character)
+ # `tool`: Traffic generator tool to be used (currently supported is `TRex`).
+ # `ip`: IP address of the traffic generator.
+ # `cores`: Specify the number of cores for TRex traffic generator. ONLY applies to trex-local.
+ # `interfaces`: Configuration of traffic generator interfaces.
+ # `interfaces.port`: The port of the traffic generator to be used (leave as 0 and 1 resp.)
+ # `interfaces.switch_port`: Leave empty (reserved for advanced use cases)
+ # `interfaces.pci`: The PCI address of the intel NIC interface associated to this port
+ # `intf_speed`: The speed of the interfaces used by the traffic generator (per direction).
+ #
+ generator_profile:
+ - name: trex-local
+ tool: TRex
+ ip: 127.0.0.1
+ cores: 3
+ interfaces:
+ - port: 0
+ switch_port:
+ pci:
+ - port: 1
+ switch_port:
+ pci:
+ intf_speed: 10Gbps
+
+# -----------------------------------------------------------------------------
+# These variables are not likely to be changed
+
+# The openrc file
+openrc_file:
+
+# General retry count
+generic_retry_count: 100
+
+# General poll period
+generic_poll_sec: 2
+
+# name of the loop VM
+loop_vm_name: 'nfvbench-loop-vm'
+
+# Default names, subnets and CIDRs for internal networks used by the script.
+# If a network with given name already exists it will be reused.
+# Otherwise a new internal network will be created with that name, subnet and CIDR.
+internal_networks:
+ # Required only when segmentation_id specified
+ physical_network:
+ left:
+ name: 'nfvbench-net0'
+ subnet: 'nfvbench-subnet0'
+ cidr: '192.168.1.0/24'
+ network_type: 'vlan'
+ segmentation_id:
+ right:
+ name: 'nfvbench-net1'
+ subnet: 'nfvbench-subnet1'
+ cidr: '192.168.2.0/24'
+ network_type: 'vlan'
+ segmentation_id:
+ middle:
+ name: 'nfvbench-net2'
+ subnet: 'nfvbench-subnet2'
+ cidr: '192.168.3.0/24'
+ network_type: 'vlan'
+ segmentation_id:
+
+# EXT chain only. Names of edge networks which will be used to send traffic via traffic generator.
+external_networks:
+ left: 'nfvbench-net0'
+ right: 'nfvbench-net1'
+
+# Use 'true' to enable VLAN tagging of packets coming from traffic generator
+# Leave empty if VLAN tagging is enabled on switch or if you want to hook directly to a NIC
+# Else by default is set to true (which is the nominal use case with TOR and trunk mode to Trex)
+vlan_tagging: true
+
+# Specify only when you want to override VLAN IDs used for tagging with own values (exactly 2).
+# Default behavior of VLAN tagging is to retrieve VLAN IDs from OpenStack networks provided above.
+# In case of VxLAN this setting is ignored and only vtep_vlan from traffic generator profile is used.
+# Example: [1998, 1999]
+vlans: []
+
+# Used only with EXT chain. MAC addresses of traffic generator ports are used as destination
+# if 'no_arp' is set to 'true'. Otherwise ARP requests are sent to find out destination MAC addresses.
+no_arp: false
+
+# Traffic Profiles
+# You can add here more profiles as needed
+# `l2frame_size` can be specified in any none zero integer value to represent the size in bytes
+# of the L2 frame, or "IMIX" to represent the standard 3-packet size mixed sequence (IMIX1).
+traffic_profile:
+ - name: traffic_profile_64B
+ l2frame_size: ['64']
+ - name: traffic_profile_IMIX
+ l2frame_size: ['IMIX']
+ - name: traffic_profile_1518B
+ l2frame_size: ['1518']
+ - name: traffic_profile_3sizes
+ l2frame_size: ['64', 'IMIX', '1518']
+
+# Traffic Configuration
+# bidirectional: to have traffic generated from both direction, set bidirectional to true
+# profile: must be one of the profiles defined in traffic_profile
+# The traffic profile can be overriden with the options --frame-size and --uni-dir
+traffic:
+ bidirectional: true
+ profile: traffic_profile_64B
+
+# Check config and connectivity only - do not generate traffic
+# Can be overriden by --no-traffic
+no_traffic: false
+
+# Do not reset tx/rx counters prior to running
+# Can be overriden by --no-reset
+no_reset: false
+
+# Test configuration
+
+# The rate pps for traffic going in reverse direction in case of unidirectional flow. Default to 1.
+unidir_reverse_traffic_pps: 1
+
+# The rate specifies if NFVbench should determine the NDR/PDR
+# or if NFVbench should just generate traffic at a given fixed rate
+# for a given duration (called "single run" mode)
+# Supported rate format:
+# NDR/PDR test: `ndr`, `pdr`, `ndr_pdr` (default)
+# Or for single run mode:
+# Packet per second: pps (e.g. `50pps`)
+# Bits per second: bps, kbps, Mbps, etc (e.g. `1Gbps`, `1000bps`)
+# Load percentage: % (e.g. `50%`)
+# Can be overridden by --rate
+rate: ndr_pdr
+
+# Default run duration (single run at given rate only)
+# Can be overridden by --duration
+duration_sec: 60
+
+# Interval between intermediate reports when interval reporting is enabled
+# Can be overridden by --interval
+interval_sec: 10
+
+# NDR / PDR configuration ZZ
+measurement:
+ # Drop rates represent the ratio of dropped packet to the total number of packets sent.
+ # Values provided here are percentages. A value of 0.01 means that at most 0.01% of all
+ # packets sent are dropped (or 1 packet every 10,000 packets sent)
+
+ # No Drop Rate in percentage; Default to 0.001%
+ NDR: 0.001
+ # Partial Drop Rate in percentage; NDR should always be less than PDR
+ PDR: 0.1
+ # The accuracy of NDR and PDR load percentiles; The actual load percentile that match NDR
+ # or PDR should be within `load_epsilon` difference than the one calculated.
+ load_epsilon: 0.1
+
+# Location where to store results in a JSON format. Must be container specific path.
+# Can be overriden by --json
+json:
+
+# Location where to store results in the NFVbench standard JSON format:
+# <service-chain-type>-<service-chain-count>-<flow-count>-<packet-sizes>.json
+# Example: PVP-1-10-64-IMIX.json
+# Must be container specific path.
+# Can be overriden by --std-json
+std_json:
+
+# Prints debug messages (verbose mode)
+# Can be overriden by --debug
+debug: false
+
+# Module and class name of factory which will be used to provide classes dynamically for other components.
+factory_module: 'nfvbench.factory'
+factory_class: 'BasicFactory' \ No newline at end of file
diff --git a/nfvbench/chain_clients.py b/nfvbench/chain_clients.py
new file mode 100644
index 0000000..4be050f
--- /dev/null
+++ b/nfvbench/chain_clients.py
@@ -0,0 +1,564 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import compute
+from glanceclient.v2 import client as glanceclient
+from log import LOG
+from neutronclient.neutron import client as neutronclient
+from novaclient.client import Client
+import os
+import time
+
+
+class StageClientException(Exception):
+ pass
+
+
+class BasicStageClient(object):
+ """Client for spawning and accessing the VM setup"""
+
+ nfvbenchvm_config_name = 'nfvbenchvm.conf'
+
+ def __init__(self, config, cred):
+ self.comp = None
+ self.image_instance = None
+ self.config = config
+ self.cred = cred
+ self.nets = []
+ self.vms = []
+ self.created_ports = []
+ self.ports = {}
+ self.compute_nodes = set([])
+ self.comp = None
+ self.neutron = None
+ self.flavor_type = {'is_reuse': True, 'flavor': None}
+ self.host_ips = None
+
+ def _ensure_vms_active(self):
+ for _ in range(self.config.generic_retry_count):
+ for i, instance in enumerate(self.vms):
+ if instance.status == 'ACTIVE':
+ continue
+ is_reuse = getattr(instance, 'is_reuse', True)
+ instance = self.comp.poll_server(instance)
+ if instance.status == 'ERROR':
+ raise StageClientException('Instance creation error: %s' %
+ instance.fault['message'])
+ if instance.status == 'ACTIVE':
+ LOG.info('Created instance: %s', instance.name)
+ self.vms[i] = instance
+ setattr(self.vms[i], 'is_reuse', is_reuse)
+ if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
+ return
+ time.sleep(self.config.generic_poll_sec)
+ raise StageClientException('Timed out waiting for VMs to spawn')
+
+ def _setup_openstack_clients(self):
+ self.session = self.cred.get_session()
+ nova_client = Client(2, session=self.session)
+ self.neutron = neutronclient.Client('2.0', session=self.session)
+ self.glance_client = glanceclient.Client('2',
+ session=self.session)
+ self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
+
+ def _lookup_network(self, network_name):
+ networks = self.neutron.list_networks(name=network_name)
+ return networks['networks'][0] if networks['networks'] else None
+
+ def _create_net(self, name, subnet, cidr, network_type=None, segmentation_id=None):
+ network = self._lookup_network(name)
+ if network:
+ phys_net = self.config.internal_networks.physical_network
+ if segmentation_id is not None and phys_net is not None:
+ if network['provider:segmentation_id'] != segmentation_id:
+ raise StageClientException("Mismatch of 'segmentation_id' for reused "
+ "network '{net}'. Network has id '{seg_id1}', "
+ "configuration requires '{seg_id2}'."
+ .format(net=name,
+ seg_id1=network['provider:segmentation_id'],
+ seg_id2=segmentation_id))
+
+ if network['provider:physical_network'] != phys_net:
+ raise StageClientException("Mismatch of 'physical_network' for reused "
+ "network '{net}'. Network has '{phys1}', "
+ "configuration requires '{phys2}'."
+ .format(net=name,
+ phys1=network['provider:physical_network'],
+ phys2=phys_net))
+
+ LOG.info('Reusing existing network: ' + name)
+ network['is_reuse'] = True
+ return network
+
+ body = {
+ 'network': {
+ 'name': name,
+ 'admin_state_up': True
+ }
+ }
+
+ if network_type:
+ body['network']['provider:network_type'] = network_type
+ phys_net = self.config.internal_networks.physical_network
+ if segmentation_id is not None and phys_net is not None:
+ body['network']['provider:segmentation_id'] = segmentation_id
+ body['network']['provider:physical_network'] = phys_net
+
+ network = self.neutron.create_network(body)['network']
+ body = {
+ 'subnet': {
+ 'name': subnet,
+ 'cidr': cidr,
+ 'network_id': network['id'],
+ 'enable_dhcp': False,
+ 'ip_version': 4,
+ 'dns_nameservers': []
+ }
+ }
+ subnet = self.neutron.create_subnet(body)['subnet']
+ # add subnet id to the network dict since it has just been added
+ network['subnets'] = [subnet['id']]
+ network['is_reuse'] = False
+ LOG.info('Created network: %s.' % name)
+ return network
+
+ def _create_port(self, net):
+ body = {
+ "port": {
+ 'network_id': net['id'],
+ 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
+ }
+ }
+ port = self.neutron.create_port(body)
+ return port['port']
+
+ def __delete_port(self, port):
+ retry = 0
+ while retry < self.config.generic_retry_count:
+ try:
+ self.neutron.delete_port(port['id'])
+ return
+ except Exception:
+ retry += 1
+ time.sleep(self.config.generic_poll_sec)
+ LOG.error('Unable to delete port: %s' % (port['id']))
+
+ def __delete_net(self, network):
+ retry = 0
+ while retry < self.config.generic_retry_count:
+ try:
+ self.neutron.delete_network(network['id'])
+ return
+ except Exception:
+ retry += 1
+ time.sleep(self.config.generic_poll_sec)
+ LOG.error('Unable to delete network: %s' % (network['name']))
+
+ def __get_server_az(self, server):
+ availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
+ host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
+ if availability_zone is None:
+ return None
+ if host is None:
+ return None
+ return availability_zone + ':' + host
+
+ def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
+ error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
+ networks = set(map(lambda net: net['name'], nets)) if nets else None
+ server_list = self.comp.get_server_list()
+ matching_servers = []
+
+ for server in server_list:
+ if name and server.name != name:
+ continue
+
+ if az and self.__get_server_az(server) != az:
+ raise StageClientException(error_msg.format('availability zones'))
+
+ if flavor_id and server.flavor['id'] != flavor_id:
+ raise StageClientException(error_msg.format('flavors'))
+
+ if networks and not set(server.networks.keys()).issuperset(networks):
+ raise StageClientException(error_msg.format('networks'))
+
+ if server.status != "ACTIVE":
+ raise StageClientException(error_msg.format('state'))
+
+ # everything matches
+ matching_servers.append(server)
+
+ return matching_servers
+
+ def _create_server(self, name, ports, az, nfvbenchvm_config):
+ port_ids = map(lambda port: {'port-id': port['id']}, ports)
+ nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
+ server = self.comp.create_server(name,
+ self.image_instance,
+ self.flavor_type['flavor'],
+ None,
+ port_ids,
+ None,
+ avail_zone=az,
+ user_data=None,
+ config_drive=True,
+ files={nfvbenchvm_config_location: nfvbenchvm_config})
+ if server:
+ setattr(server, 'is_reuse', False)
+ LOG.info('Creating instance: %s on %s' % (name, az))
+ else:
+ raise StageClientException('Unable to create instance: %s.' % (name))
+ return server
+
+ def _setup_resources(self):
+ if not self.image_instance:
+ self.image_instance = self.comp.find_image(self.config.image_name)
+ if self.image_instance is None:
+ if self.config.vm_image_file:
+ LOG.info('%s: image for VM not found, trying to upload it ...'
+ % self.config.image_name)
+ res = self.comp.upload_image_via_url(self.config.image_name,
+ self.config.vm_image_file)
+
+ if not res:
+ raise StageClientException('Error uploading image %s from %s. ABORTING.'
+ % (self.config.image_name,
+ self.config.vm_image_file))
+ self.image_instance = self.comp.find_image(self.config.image_name)
+ else:
+ raise StageClientException('%s: image to launch VM not found. ABORTING.'
+ % self.config.image_name)
+
+ LOG.info('Found image %s to launch VM' % self.config.image_name)
+
+ self.__setup_flavor()
+
+ def __setup_flavor(self):
+ if self.flavor_type.get('flavor', False):
+ return
+
+ self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
+ if self.flavor_type['flavor']:
+ self.flavor_type['is_reuse'] = True
+ else:
+ flavor_dict = self.config.flavor
+ extra_specs = flavor_dict.pop('extra_specs', None)
+
+ self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
+ override=True,
+ **flavor_dict)
+
+ LOG.info("Flavor '%s' was created." % self.config.flavor_type)
+
+ if extra_specs:
+ self.flavor_type['flavor'].set_keys(extra_specs)
+
+ self.flavor_type['is_reuse'] = False
+
+ if self.flavor_type['flavor'] is None:
+ raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
+ % self.config.flavor_type)
+
+ def __delete_flavor(self, flavor):
+ if self.comp.delete_flavor(flavor=flavor):
+ LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
+ self.flavor_type = {'is_reuse': False, 'flavor': None}
+ else:
+ LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
+
+ def get_config_file(self, chain_index, src_mac, dst_mac):
+ boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ 'nfvbenchvm/', self.nfvbenchvm_config_name)
+
+ with open(boot_script_file, 'r') as boot_script:
+ content = boot_script.read()
+
+ g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
+ g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
+
+ vm_config = {
+ 'forwarder': self.config.vm_forwarder,
+ 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
+ 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
+ 'tg_net1': self.config.traffic_generator.ip_addrs[0],
+ 'tg_net2': self.config.traffic_generator.ip_addrs[1],
+ 'vnf_gateway1_cidr': g1cidr,
+ 'vnf_gateway2_cidr': g2cidr,
+ 'tg_mac1': src_mac,
+ 'tg_mac2': dst_mac
+ }
+
+ return content.format(**vm_config)
+
+ def set_ports(self):
+ """Stores all ports of NFVbench networks."""
+ nets = self.get_networks_uuids()
+ for port in self.neutron.list_ports()['ports']:
+ if port['network_id'] in nets:
+ ports = self.ports.setdefault(port['network_id'], [])
+ ports.append(port)
+
+ def disable_port_security(self):
+ """
+ Disable security at port level.
+ """
+ vm_ids = map(lambda vm: vm.id, self.vms)
+ for net in self.nets:
+ for port in self.ports[net['id']]:
+ if port['device_id'] in vm_ids:
+ self.neutron.update_port(port['id'], {
+ 'port': {
+ 'security_groups': [],
+ 'port_security_enabled': False,
+ }
+ })
+ LOG.info('Security disabled on port {}'.format(port['id']))
+
+ def get_loop_vm_hostnames(self):
+ return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
+
+ def get_host_ips(self):
+ '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
+ Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
+ '''
+ if not self.host_ips:
+ # get the hypervisor object from the host name
+ self.host_ips = [self.comp.get_hypervisor(
+ getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
+ for vm in self.vms]
+ return self.host_ips
+
+ def get_loop_vm_compute_nodes(self):
+ compute_nodes = []
+ for vm in self.vms:
+ az = getattr(vm, 'OS-EXT-AZ:availability_zone')
+ hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
+ compute_nodes.append(az + ':' + hostname)
+ return compute_nodes
+
+ def get_reusable_vm(self, name, nets, az):
+ servers = self._lookup_servers(name=name, nets=nets, az=az,
+ flavor_id=self.flavor_type['flavor'].id)
+ if servers:
+ server = servers[0]
+ LOG.info('Reusing existing server: ' + name)
+ setattr(server, 'is_reuse', True)
+ return server
+ else:
+ return None
+
+ def get_networks_uuids(self):
+ """
+ Extract UUID of used networks. Order is important.
+
+ :return: list of UUIDs of created networks
+ """
+ return [net['id'] for net in self.nets]
+
+ def get_vlans(self):
+ """
+ Extract vlans of used networks. Order is important.
+
+ :return: list of UUIDs of created networks
+ """
+ vlans = []
+ for net in self.nets:
+ assert(net['provider:network_type'] == 'vlan')
+ vlans.append(net['provider:segmentation_id'])
+
+ return vlans
+
+ def setup(self):
+ """
+ Creates two networks and spawn a VM which act as a loop VM connected
+ with the two networks.
+ """
+ self._setup_openstack_clients()
+
+ def dispose(self, only_vm=False):
+ """
+ Deletes the created two networks and the VM.
+ """
+ for vm in self.vms:
+ if vm:
+ if not getattr(vm, 'is_reuse', True):
+ self.comp.delete_server(vm)
+ else:
+ LOG.info('Server %s not removed since it is reused' % vm.name)
+
+ for port in self.created_ports:
+ self.__delete_port(port)
+
+ if not only_vm:
+ for net in self.nets:
+ if 'is_reuse' in net and not net['is_reuse']:
+ self.__delete_net(net)
+ else:
+ LOG.info('Network %s not removed since it is reused' % (net['name']))
+
+ if not self.flavor_type['is_reuse']:
+ self.__delete_flavor(self.flavor_type['flavor'])
+
+
+class EXTStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(EXTStageClient, self).__init__(config, cred)
+
+ def setup(self):
+ super(EXTStageClient, self).setup()
+
+ # Lookup two existing networks
+ for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
+ net = self._lookup_network(net_name)
+ if net:
+ self.nets.append(net)
+ else:
+ raise StageClientException('Existing network {} cannot be found.'.format(net_name))
+
+
+class PVPStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(PVPStageClient, self).__init__(config, cred)
+
+ def get_end_port_macs(self):
+ vm_ids = map(lambda vm: vm.id, self.vms)
+ port_macs = []
+ for index, net in enumerate(self.nets):
+ vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
+ port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
+ return port_macs
+
+ def setup(self):
+ super(PVPStageClient, self).setup()
+ self._setup_resources()
+
+ # Create two networks
+ nets = self.config.internal_networks
+ self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
+
+ az_list = self.comp.get_enabled_az_host_list(required_count=1)
+ if not az_list:
+ raise Exception('Not enough hosts found.')
+
+ az = az_list[0]
+ self.compute_nodes.add(az)
+ for chain_index in xrange(self.config.service_chain_count):
+ name = self.config.loop_vm_name + str(chain_index)
+ reusable_vm = self.get_reusable_vm(name, self.nets, az)
+ if reusable_vm:
+ self.vms.append(reusable_vm)
+ else:
+ config_file = self.get_config_file(chain_index,
+ self.config.generator_config.src_device.mac,
+ self.config.generator_config.dst_device.mac)
+
+ ports = [self._create_port(net) for net in self.nets]
+ self.created_ports.extend(ports)
+ self.vms.append(self._create_server(name, ports, az, config_file))
+ self._ensure_vms_active()
+ self.set_ports()
+
+
+class PVVPStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(PVVPStageClient, self).__init__(config, cred)
+
+ def get_end_port_macs(self):
+ port_macs = []
+ for index, net in enumerate(self.nets[:2]):
+ vm_ids = map(lambda vm: vm.id, self.vms[index::2])
+ vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
+ port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
+ return port_macs
+
+ def setup(self):
+ super(PVVPStageClient, self).setup()
+ self._setup_resources()
+
+ # Create two networks
+ nets = self.config.internal_networks
+ self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
+
+ required_count = 2 if self.config.inter_node else 1
+ az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
+
+ if not az_list:
+ raise Exception('Not enough hosts found.')
+
+ az1 = az2 = az_list[0]
+ if self.config.inter_node:
+ if len(az_list) > 1:
+ az1 = az_list[0]
+ az2 = az_list[1]
+ else:
+ # fallback to intra-node
+ az1 = az2 = az_list[0]
+ self.config.inter_node = False
+ LOG.info('Using intra-node instead of inter-node.')
+
+ self.compute_nodes.add(az1)
+ self.compute_nodes.add(az2)
+
+ # Create loop VMs
+ for chain_index in xrange(self.config.service_chain_count):
+ name0 = self.config.loop_vm_name + str(chain_index) + 'a'
+ # Attach first VM to net0 and net2
+ vm0_nets = self.nets[0::2]
+ reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
+
+ name1 = self.config.loop_vm_name + str(chain_index) + 'b'
+ # Attach second VM to net1 and net2
+ vm1_nets = self.nets[1:]
+ reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
+
+ if reusable_vm0 and reusable_vm1:
+ self.vms.extend([reusable_vm0, reusable_vm1])
+ else:
+ vm0_port_net0 = self._create_port(vm0_nets[0])
+ vm0_port_net2 = self._create_port(vm0_nets[1])
+
+ vm1_port_net2 = self._create_port(vm1_nets[1])
+ vm1_port_net1 = self._create_port(vm1_nets[0])
+
+ self.created_ports.extend([vm0_port_net0,
+ vm0_port_net2,
+ vm1_port_net2,
+ vm1_port_net1])
+
+ # order of ports is important for sections below
+ # order of MAC addresses needs to follow order of interfaces
+ # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
+ config_file0 = self.get_config_file(chain_index,
+ self.config.generator_config.src_device.mac,
+ vm1_port_net2['mac_address'])
+ config_file1 = self.get_config_file(chain_index,
+ vm0_port_net2['mac_address'],
+ self.config.generator_config.dst_device.mac)
+
+ self.vms.append(self._create_server(name0,
+ [vm0_port_net0, vm0_port_net2],
+ az1,
+ config_file0))
+ self.vms.append(self._create_server(name1,
+ [vm1_port_net2, vm1_port_net1],
+ az2,
+ config_file1))
+
+ self._ensure_vms_active()
+ self.set_ports()
diff --git a/nfvbench/chain_managers.py b/nfvbench/chain_managers.py
new file mode 100644
index 0000000..fe3a2d4
--- /dev/null
+++ b/nfvbench/chain_managers.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from log import LOG
+from network import Network
+from packet_analyzer import PacketAnalyzer
+from specs import ChainType
+from stats_collector import IntervalCollector
+import time
+
+
+class StageManager(object):
+
+ def __init__(self, config, cred, factory):
+ self.config = config
+ self.client = None
+ # conditions due to EXT chain special cases
+ if (config.vlan_tagging and not config.vlans) or not config.no_int_config:
+ VM_CLASS = factory.get_stage_class(config.service_chain)
+ self.client = VM_CLASS(config, cred)
+ self.client.setup()
+
+ def get_vlans(self):
+ return self.client.get_vlans() if self.client else []
+
+ def get_host_ips(self):
+ return self.client.get_host_ips()
+
+ def get_networks_uuids(self):
+ return self.client.get_networks_uuids()
+
+ def disable_port_security(self):
+ self.client.disable_port_security()
+
+ def get_vms(self):
+ return self.client.vms
+
+ def get_nets(self):
+ return self.client.nets
+
+ def get_ports(self):
+ return self.client.ports
+
+ def get_compute_nodes(self):
+ return self.client.compute_nodes
+
+ def set_vm_macs(self):
+ if self.client and self.config.service_chain != ChainType.EXT:
+ self.config.generator_config.set_vm_mac_list(self.client.get_end_port_macs())
+
+ def close(self):
+ if not self.config.no_cleanup and self.client:
+ self.client.dispose()
+
+
+class StatsManager(object):
+
+ def __init__(self, config, clients, specs, factory, vlans, notifier=None):
+ self.config = config
+ self.clients = clients
+ self.specs = specs
+ self.notifier = notifier
+ self.interval_collector = None
+ self.vlans = vlans
+ self.factory = factory
+ self._setup()
+
+ def set_vlan_tag(self, device, vlan):
+ self.worker.set_vlan_tag(device, vlan)
+
+ def _setup(self):
+ WORKER_CLASS = self.factory.get_chain_worker(self.specs.openstack.encaps,
+ self.config.service_chain)
+ self.worker = WORKER_CLASS(self.config, self.clients, self.specs)
+ self.worker.set_vlans(self.vlans)
+ self._config_interfaces()
+
+ def _get_data(self):
+ return self.worker.get_data()
+
+ def _get_network(self, traffic_port, index=None, reverse=False):
+ interfaces = [self.clients['traffic'].get_interface(traffic_port)]
+ interfaces.extend(self.worker.get_network_interfaces(index))
+ return Network(interfaces, reverse)
+
+ def _config_interfaces(self):
+ if self.config.service_chain != ChainType.EXT:
+ self.clients['vm'].disable_port_security()
+
+ self.worker.config_interfaces()
+
+ def _generate_traffic(self):
+ if self.config.no_traffic:
+ return
+
+ self.interval_collector = IntervalCollector(time.time())
+ self.interval_collector.attach_notifier(self.notifier)
+ LOG.info('Starting to generate traffic...')
+ stats = {}
+ for stats in self.clients['traffic'].run_traffic():
+ self.interval_collector.add(stats)
+
+ LOG.info('...traffic generating ended.')
+ return stats
+
+ def get_stats(self):
+ return self.interval_collector.get() if self.interval_collector else []
+
+ def get_version(self):
+ return self.worker.get_version()
+
+ def run(self):
+ """
+ Run analysis in both direction and return the analysis
+ """
+ self.worker.run()
+
+ stats = self._generate_traffic()
+ result = {
+ 'raw_data': self._get_data(),
+ 'packet_analysis': {},
+ 'stats': stats
+ }
+
+ LOG.info('Requesting packet analysis on the forward direction...')
+ result['packet_analysis']['direction-forward'] = \
+ self.get_analysis([self._get_network(0, 0),
+ self._get_network(0, 1, reverse=True)])
+ LOG.info('Packet analysis on the forward direction completed')
+
+ LOG.info('Requesting packet analysis on the reverse direction...')
+ result['packet_analysis']['direction-reverse'] = \
+ self.get_analysis([self._get_network(1, 1),
+ self._get_network(1, 0, reverse=True)])
+
+ LOG.info('Packet analysis on the reverse direction completed')
+ return result
+
+ def get_compute_nodes_bios(self):
+ return self.worker.get_compute_nodes_bios()
+
+ @staticmethod
+ def get_analysis(nets):
+ LOG.info('Starting traffic analysis...')
+
+ packet_analyzer = PacketAnalyzer()
+ # Traffic types are assumed to always alternate in every chain. Add a no stats interface in
+ # between if that is not the case.
+ tx = True
+ for network in nets:
+ for interface in network.get_interfaces():
+ packet_analyzer.record(interface, 'tx' if tx else 'rx')
+ tx = not tx
+
+ LOG.info('...traffic analysis completed')
+ return packet_analyzer.get_analysis()
+
+ def close(self):
+ self.worker.close()
+
+
+class PVPStatsManager(StatsManager):
+
+ def __init__(self, config, clients, specs, factory, vlans, notifier=None):
+ StatsManager.__init__(self, config, clients, specs, factory, vlans, notifier)
+
+
+class PVVPStatsManager(StatsManager):
+
+ def __init__(self, config, clients, specs, factory, vlans, notifier=None):
+ StatsManager.__init__(self, config, clients, specs, factory, vlans, notifier)
+
+ def run(self):
+ """
+ Run analysis in both direction and return the analysis
+ """
+ fwd_v2v_net, rev_v2v_net = self.worker.run()
+
+ stats = self._generate_traffic()
+ result = {
+ 'raw_data': self._get_data(),
+ 'packet_analysis': {},
+ 'stats': stats
+ }
+
+ fwd_nets = [self._get_network(0, 0)]
+ if fwd_v2v_net:
+ fwd_nets.append(fwd_v2v_net)
+ fwd_nets.append(self._get_network(0, 1, reverse=True))
+
+ rev_nets = [self._get_network(1, 1)]
+ if rev_v2v_net:
+ rev_nets.append(rev_v2v_net)
+ rev_nets.append(self._get_network(1, 0, reverse=True))
+
+ LOG.info('Requesting packet analysis on the forward direction...')
+ result['packet_analysis']['direction-forward'] = self.get_analysis(fwd_nets)
+ LOG.info('Packet analysis on the forward direction completed')
+
+ LOG.info('Requesting packet analysis on the reverse direction...')
+ result['packet_analysis']['direction-reverse'] = self.get_analysis(rev_nets)
+
+ LOG.info('Packet analysis on the reverse direction completed')
+ return result
+
+
+class EXTStatsManager(StatsManager):
+ def __init__(self, config, clients, specs, factory, vlans, notifier=None):
+ StatsManager.__init__(self, config, clients, specs, factory, vlans, notifier)
+
+ def _setup(self):
+ WORKER_CLASS = self.factory.get_chain_worker(self.specs.openstack.encaps,
+ self.config.service_chain)
+ self.worker = WORKER_CLASS(self.config, self.clients, self.specs)
+ self.worker.set_vlans(self.vlans)
+
+ if not self.config.no_int_config:
+ self._config_interfaces()
diff --git a/nfvbench/chain_runner.py b/nfvbench/chain_runner.py
new file mode 100644
index 0000000..2e222de
--- /dev/null
+++ b/nfvbench/chain_runner.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from log import LOG
+from service_chain import ServiceChain
+import traceback
+from traffic_client import TrafficClient
+
+
+class ChainRunner(object):
+ """Run selected chain, collect results and analyse them."""
+
+ def __init__(self, config, clients, cred, specs, factory, notifier=None):
+ self.config = config
+ self.clients = clients
+ self.specs = specs
+ self.factory = factory
+ self.chain_name = self.config.service_chain
+
+ try:
+ TORClass = factory.get_tor_class(self.config.tor.type, self.config.no_tor_access)
+ except AttributeError:
+ raise Exception("Requested TOR class '{}' was not found.".format(self.config.tor.type))
+
+ self.clients['tor'] = TORClass(self.config.tor.switches)
+ self.clients['traffic'] = TrafficClient(config, notifier)
+ self.chain = ServiceChain(config, clients, cred, specs, factory, notifier)
+
+ LOG.info('ChainRunner initialized.')
+
+ def run(self):
+ """
+ Run a chain, collect and analyse results.
+
+ :return: dictionary
+ """
+ self.clients['traffic'].start_traffic_generator()
+ self.clients['traffic'].set_macs()
+
+ return self.chain.run()
+
+ def close(self):
+ try:
+ if not self.config.no_cleanup:
+ LOG.info('Cleaning up...')
+ else:
+ LOG.info('Clean up skipped.')
+
+ for client in ['traffic', 'tor']:
+ try:
+ self.clients[client].close()
+ except Exception as e:
+ traceback.print_exc()
+ LOG.error(e)
+
+ self.chain.close()
+ except Exception:
+ traceback.print_exc()
+ LOG.error('Cleanup not finished.')
+
+ def get_version(self):
+ versions = {
+ 'Traffic Generator': self.clients['traffic'].get_version(),
+ 'TOR': self.clients['tor'].get_version(),
+ }
+
+ versions.update(self.chain.get_version())
+
+ return versions
diff --git a/nfvbench/chain_workers.py b/nfvbench/chain_workers.py
new file mode 100644
index 0000000..2e36fb1
--- /dev/null
+++ b/nfvbench/chain_workers.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+
+class BasicWorker(object):
+
+ def __init__(self, config, clients, specs):
+ self.config = config
+ self.clients = clients
+ self.specs = specs
+
+ def set_vlan_tag(self, device, vlan):
+ device.set_vlan_tag(vlan)
+
+ def set_vlans(self, vlans):
+ pass
+
+ def config_interfaces(self):
+ pass
+
+ def get_data(self):
+ return {}
+
+ def get_network_interfaces(self, index):
+ return []
+
+ def clear_interfaces(self):
+ pass
+
+ def run(self):
+ return None, None
+
+ def get_compute_nodes_bios(self):
+ return {}
+
+ def get_version(self):
+ return {}
+
+ def close(self):
+ pass
diff --git a/nfvbench/compute.py b/nfvbench/compute.py
new file mode 100644
index 0000000..c8ec383
--- /dev/null
+++ b/nfvbench/compute.py
@@ -0,0 +1,483 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+'''Module for Openstack compute operations'''
+from glanceclient import exc as glance_exception
+import keystoneauth1
+from log import LOG
+import novaclient
+import os
+import time
+import traceback
+
+
+try:
+ from glanceclient.openstack.common.apiclient.exceptions import NotFound as GlanceImageNotFound
+except ImportError:
+ from glanceclient.v1.apiclient.exceptions import NotFound as GlanceImageNotFound
+
+
+class Compute(object):
+
+ def __init__(self, nova_client, glance_client, neutron_client, config):
+ self.novaclient = nova_client
+ self.glance_client = glance_client
+ self.neutronclient = neutron_client
+ self.config = config
+
+ def find_image(self, image_name):
+ try:
+ return next(self.glance_client.images.list(filters={'name': image_name}), None)
+ except (novaclient.exceptions.NotFound, keystoneauth1.exceptions.http.NotFound,
+ GlanceImageNotFound):
+ pass
+ return None
+
+ def upload_image_via_url(self, final_image_name, image_file, retry_count=60):
+ '''
+ Directly uploads image to Nova via URL if image is not present
+ '''
+ retry = 0
+ try:
+ # check image is file/url based.
+ file_prefix = "file://"
+ image_location = image_file.split(file_prefix)[1]
+ with open(image_location) as f_image:
+ img = self.glance_client.images.create(name=str(final_image_name),
+ disk_format="qcow2",
+ container_format="bare",
+ visibility="public")
+ self.glance_client.images.upload(img.id, image_data=f_image)
+ # Check for the image in glance
+ while img.status in ['queued', 'saving'] and retry < retry_count:
+ img = self.glance_client.images.get(img.id)
+ retry += 1
+ LOG.debug("Image not yet active, retrying %s of %s...", retry, retry_count)
+ time.sleep(self.config.generic_poll_sec)
+ if img.status != 'active':
+ LOG.error("Image uploaded but too long to get to active state")
+ raise Exception("Image update active state timeout")
+ except glance_exception.HTTPForbidden:
+ LOG.error("Cannot upload image without admin access. Please make "
+ "sure the image is uploaded and is either public or owned by you.")
+ return False
+ except IOError:
+ # catch the exception for file based errors.
+ LOG.error("Failed while uploading the image. Please make sure the "
+ "image at the specified location %s is correct.", image_file)
+ return False
+ except keystoneauth1.exceptions.http.NotFound as exc:
+ LOG.error("Authentication error while uploading the image:" + str(exc))
+ return False
+ except Exception:
+ LOG.error(traceback.format_exc())
+ LOG.error("Failed while uploading the image, please make sure the "
+ "cloud under test has the access to file: %s.", image_file)
+ return False
+ return True
+
+ def delete_image(self, img_name):
+ try:
+ LOG.log("Deleting image %s...", img_name)
+ img = self.glance_client.images.find(name=img_name)
+ self.glance_client.images.delete(img.id)
+ except Exception:
+ LOG.error("Failed to delete the image %s.", img_name)
+ return False
+
+ return True
+
+ # Remove keypair name from openstack if exists
+ def remove_public_key(self, name):
+ keypair_list = self.novaclient.keypairs.list()
+ for key in keypair_list:
+ if key.name == name:
+ self.novaclient.keypairs.delete(name)
+ LOG.info('Removed public key %s', name)
+ break
+
+ # Test if keypair file is present if not create it
+ def create_keypair(self, name, private_key_pair_file):
+ self.remove_public_key(name)
+ keypair = self.novaclient.keypairs.create(name)
+ # Now write the keypair to the file if requested
+ if private_key_pair_file:
+ kpf = os.open(private_key_pair_file,
+ os.O_WRONLY | os.O_CREAT, 0o600)
+ with os.fdopen(kpf, 'w') as kpf:
+ kpf.write(keypair.private_key)
+ return keypair
+
+ # Add an existing public key to openstack
+ def add_public_key(self, name, public_key_file):
+ self.remove_public_key(name)
+ # extract the public key from the file
+ public_key = None
+ try:
+ with open(os.path.expanduser(public_key_file)) as pkf:
+ public_key = pkf.read()
+ except IOError as exc:
+ LOG.error('Cannot open public key file %s: %s', public_key_file, exc)
+ return None
+ keypair = self.novaclient.keypairs.create(name, public_key)
+ return keypair
+
+ def init_key_pair(self, kp_name, ssh_access):
+ '''Initialize the key pair for all test VMs
+ if a key pair is specified in access, use that key pair else
+ create a temporary key pair
+ '''
+ if ssh_access.public_key_file:
+ return self.add_public_key(kp_name, ssh_access.public_key_file)
+ else:
+ keypair = self.create_keypair(kp_name, None)
+ ssh_access.private_key = keypair.private_key
+ return keypair
+
+ def find_network(self, label):
+ net = self.novaclient.networks.find(label=label)
+ return net
+
+ # Create a server instance with name vmname
+ # and check that it gets into the ACTIVE state
+ def create_server(self, vmname, image, flavor, key_name,
+ nic, sec_group, avail_zone=None, user_data=None,
+ config_drive=None, files=None):
+
+ if sec_group:
+ security_groups = [sec_group['id']]
+ else:
+ security_groups = None
+
+ # Also attach the created security group for the test
+ instance = self.novaclient.servers.create(name=vmname,
+ image=image,
+ flavor=flavor,
+ key_name=key_name,
+ nics=nic,
+ availability_zone=avail_zone,
+ userdata=user_data,
+ config_drive=config_drive,
+ files=files,
+ security_groups=security_groups)
+ return instance
+
+ def poll_server(self, instance):
+ return self.novaclient.servers.get(instance.id)
+
+ def get_server_list(self):
+ servers_list = self.novaclient.servers.list()
+ return servers_list
+
+ def find_floating_ips(self):
+ floating_ip = self.novaclient.floating_ips.list()
+ return floating_ip
+
+ def create_floating_ips(self, pool):
+ return self.novaclient.floating_ips.create(pool)
+
+ # Return the server network for a server
+ def find_server_network(self, vmname):
+ servers_list = self.get_server_list()
+ for server in servers_list:
+ if server.name == vmname and server.status == "ACTIVE":
+ return server.networks
+ return None
+
+ # Returns True if server is present false if not.
+ # Retry for a few seconds since after VM creation sometimes
+ # it takes a while to show up
+ def find_server(self, vmname, retry_count):
+ for retry_attempt in range(retry_count):
+ servers_list = self.get_server_list()
+ for server in servers_list:
+ if server.name == vmname and server.status == "ACTIVE":
+ return True
+ # Sleep between retries
+ LOG.debug("[%s] VM not yet found, retrying %s of %s...",
+ vmname, (retry_attempt + 1), retry_count)
+ time.sleep(self.config.generic_poll_sec)
+ LOG.error("[%s] VM not found, after %s attempts", vmname, retry_count)
+ return False
+
+ # Returns True if server is found and deleted/False if not,
+ # retry the delete if there is a delay
+ def delete_server_by_name(self, vmname):
+ servers_list = self.get_server_list()
+ for server in servers_list:
+ if server.name == vmname:
+ LOG.info('Deleting server %s', server)
+ self.novaclient.servers.delete(server)
+ return True
+ return False
+
+ def delete_server(self, server):
+ self.novaclient.servers.delete(server)
+
+ def find_flavor(self, flavor_type):
+ try:
+ flavor = self.novaclient.flavors.find(name=flavor_type)
+ return flavor
+ except Exception:
+ return None
+
+ def create_flavor(self, name, ram, vcpus, disk, ephemeral=0, override=False):
+ if override:
+ self.delete_flavor(name)
+ return self.novaclient.flavors.create(name=name, ram=ram, vcpus=vcpus, disk=disk,
+ ephemeral=ephemeral)
+
+ def delete_flavor(self, flavor=None, name=None):
+ try:
+ if not flavor:
+ flavor = self.find_flavor(name)
+ flavor.delete()
+ return True
+ except Exception:
+ return False
+
+ def normalize_az_host(self, az, host):
+ if not az:
+ az = self.config.availability_zone
+ return az + ':' + host
+
+ def auto_fill_az(self, host_list, host):
+ '''
+ no az provided, if there is a host list we can auto-fill the az
+ else we use the configured az if available
+ else we return an error
+ '''
+ if host_list:
+ for hyp in host_list:
+ if hyp.host == host:
+ return self.normalize_az_host(hyp.zone, host)
+ # no match on host
+ LOG.error('Passed host name does not exist: ' + host)
+ return None
+ if self.config.availability_zone:
+ return self.normalize_az_host(None, host)
+ LOG.error('--hypervisor passed without an az and no az configured')
+ return None
+
+ def sanitize_az_host(self, host_list, az_host):
+ '''
+ host_list: list of hosts as retrieved from openstack (can be empty)
+ az_host: either a host or a az:host string
+ if a host, will check host is in the list, find the corresponding az and
+ return az:host
+ if az:host is passed will check the host is in the list and az matches
+ if host_list is empty, will return the configured az if there is no
+ az passed
+ '''
+ if ':' in az_host:
+ # no host_list, return as is (no check)
+ if not host_list:
+ return az_host
+ # if there is a host_list, extract and verify the az and host
+ az_host_list = az_host.split(':')
+ zone = az_host_list[0]
+ host = az_host_list[1]
+ for hyp in host_list:
+ if hyp.host == host:
+ if hyp.zone == zone:
+ # matches
+ return az_host
+ # else continue - another zone with same host name?
+ # no match
+ LOG.error('No match for availability zone and host ' + az_host)
+ return None
+ else:
+ return self.auto_fill_az(host_list, az_host)
+
+ #
+ # Return a list of 0, 1 or 2 az:host
+ #
+ # The list is computed as follows:
+ # The list of all hosts is retrieved first from openstack
+ # if this fails, checks and az auto-fill are disabled
+ #
+ # If the user provides a list of hypervisors (--hypervisor)
+ # that list is checked and returned
+ #
+ # If the user provides a configured az name (config.availability_zone)
+ # up to the first 2 hosts from the list that match the az are returned
+ #
+ # If the user did not configure an az name
+ # up to the first 2 hosts from the list are returned
+ # Possible return values:
+ # [ az ]
+ # [ az:hyp ]
+ # [ az1:hyp1, az2:hyp2 ]
+ # [] if an error occurred (error message printed to console)
+ #
+ def get_az_host_list(self):
+ avail_list = []
+ host_list = []
+
+ try:
+ host_list = self.novaclient.services.list()
+ except novaclient.exceptions.Forbidden:
+ LOG.warning('Operation Forbidden: could not retrieve list of hosts'
+ ' (likely no permission)')
+
+ for host in host_list:
+ # this host must be a compute node
+ if host.binary != 'nova-compute' or host.state != 'up':
+ continue
+ candidate = None
+ if self.config.availability_zone:
+ if host.zone == self.config.availability_zone:
+ candidate = self.normalize_az_host(None, host.host)
+ else:
+ candidate = self.normalize_az_host(host.zone, host.host)
+ if candidate:
+ avail_list.append(candidate)
+ # pick first 2 matches at most
+ if len(avail_list) == 2:
+ break
+
+ # if empty we insert the configured az
+ if not avail_list:
+
+ if not self.config.availability_zone:
+ LOG.error('Availability_zone must be configured')
+ elif host_list:
+ LOG.error('No host matching the selection for availability zone: ' +
+ self.config.availability_zone)
+ avail_list = []
+ else:
+ avail_list = [self.config.availability_zone]
+ return avail_list
+
+ def get_enabled_az_host_list(self, required_count=1):
+ """
+ Check which hypervisors are enabled and on which compute nodes they are running.
+ Pick required count of hosts.
+
+ :param required_count: count of compute-nodes to return
+ :return: list of enabled available compute nodes
+ """
+ host_list = []
+ hypervisor_list = []
+
+ try:
+ hypervisor_list = self.novaclient.hypervisors.list()
+ host_list = self.novaclient.services.list()
+ except novaclient.exceptions.Forbidden:
+ LOG.warning('Operation Forbidden: could not retrieve list of hypervisors'
+ ' (likely no permission)')
+
+ hypervisor_list = filter(lambda h: h.status == 'enabled' and h.state == 'up',
+ hypervisor_list)
+ if self.config.availability_zone:
+ host_list = filter(lambda h: h.zone == self.config.availability_zone, host_list)
+
+ if self.config.compute_nodes:
+ host_list = filter(lambda h: h.host in self.config.compute_nodes, host_list)
+
+ hosts = [h.hypervisor_hostname for h in hypervisor_list]
+ host_list = filter(lambda h: h.host in hosts, host_list)
+
+ avail_list = []
+ for host in host_list:
+ candidate = self.normalize_az_host(host.zone, host.host)
+ if candidate:
+ avail_list.append(candidate)
+ if len(avail_list) == required_count:
+ return avail_list
+
+ return avail_list
+
+ def get_hypervisor(self, hyper_name):
+ # can raise novaclient.exceptions.NotFound
+ # first get the id from name
+ hyper = self.novaclient.hypervisors.search(hyper_name)[0]
+ # get full hypervisor object
+ return self.novaclient.hypervisors.get(hyper.id)
+
+ # Given 2 VMs test if they are running on same Host or not
+ def check_vm_placement(self, vm_instance1, vm_instance2):
+ try:
+ server_instance_1 = self.novaclient.servers.get(vm_instance1)
+ server_instance_2 = self.novaclient.servers.get(vm_instance2)
+ if server_instance_1.hostId == server_instance_2.hostId:
+ return True
+ else:
+ return False
+ except novaclient.exceptions:
+ LOG.warning("Exception in retrieving the hostId of servers")
+
+ # Create a new security group with appropriate rules
+ def security_group_create(self):
+ # check first the security group exists
+ sec_groups = self.neutronclient.list_security_groups()['security_groups']
+ group = [x for x in sec_groups if x['name'] == self.config.security_group_name]
+ if len(group) > 0:
+ return group[0]
+
+ body = {
+ 'security_group': {
+ 'name': self.config.security_group_name,
+ 'description': 'PNS Security Group'
+ }
+ }
+ group = self.neutronclient.create_security_group(body)['security_group']
+ self.security_group_add_rules(group)
+
+ return group
+
+ # Delete a security group
+ def security_group_delete(self, group):
+ if group:
+ LOG.info("Deleting security group")
+ self.neutronclient.delete_security_group(group['id'])
+
+ # Add rules to the security group
+ def security_group_add_rules(self, group):
+ body = {
+ 'security_group_rule': {
+ 'direction': 'ingress',
+ 'security_group_id': group['id'],
+ 'remote_group_id': None
+ }
+ }
+ if self.config.ipv6_mode:
+ body['security_group_rule']['ethertype'] = 'IPv6'
+ body['security_group_rule']['remote_ip_prefix'] = '::/0'
+ else:
+ body['security_group_rule']['ethertype'] = 'IPv4'
+ body['security_group_rule']['remote_ip_prefix'] = '0.0.0.0/0'
+
+ # Allow ping traffic
+ body['security_group_rule']['protocol'] = 'icmp'
+ body['security_group_rule']['port_range_min'] = None
+ body['security_group_rule']['port_range_max'] = None
+ self.neutronclient.create_security_group_rule(body)
+
+ # Allow SSH traffic
+ body['security_group_rule']['protocol'] = 'tcp'
+ body['security_group_rule']['port_range_min'] = 22
+ body['security_group_rule']['port_range_max'] = 22
+ self.neutronclient.create_security_group_rule(body)
+
+ # Allow TCP/UDP traffic for perf tools like iperf/nuttcp
+ # 5001: Data traffic (standard iperf data port)
+ # 5002: Control traffic (non standard)
+ # note that 5000/tcp is already picked by openstack keystone
+ body['security_group_rule']['protocol'] = 'tcp'
+ body['security_group_rule']['port_range_min'] = 5001
+ body['security_group_rule']['port_range_max'] = 5002
+ self.neutronclient.create_security_group_rule(body)
+ body['security_group_rule']['protocol'] = 'udp'
+ self.neutronclient.create_security_group_rule(body)
diff --git a/nfvbench/config.py b/nfvbench/config.py
new file mode 100644
index 0000000..b2972dd
--- /dev/null
+++ b/nfvbench/config.py
@@ -0,0 +1,56 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from attrdict import AttrDict
+import yaml
+
+
+def config_load(file_name, from_cfg=None):
+ """Load a yaml file into a config dict, merge with from_cfg if not None
+ The config file content taking precedence in case of duplicate
+ """
+ try:
+ with open(file_name) as fileobj:
+ cfg = AttrDict(yaml.safe_load(fileobj))
+ except IOError:
+ raise Exception("Configuration file at '{}' was not found. Please use correct path "
+ "and verify it is visible to container if you run nfvbench in container."
+ .format(file_name))
+
+ if from_cfg:
+ cfg = from_cfg + cfg
+
+ return cfg
+
+
+def config_loads(cfg_text, from_cfg=None):
+ """Same as config_load but load from a string
+ """
+ try:
+ cfg = AttrDict(yaml.load(cfg_text))
+ except TypeError:
+ # empty string
+ cfg = AttrDict()
+ if from_cfg:
+ return from_cfg + cfg
+ return cfg
+
+
+def test_config():
+ cfg = config_load('a1.yaml')
+ cfg = config_load('a2.yaml', cfg)
+ cfg = config_loads('color: 500', cfg)
+ config_loads('')
+ config_loads('#')
diff --git a/nfvbench/config_plugin.py b/nfvbench/config_plugin.py
new file mode 100644
index 0000000..ed6b3c6
--- /dev/null
+++ b/nfvbench/config_plugin.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import abc
+import specs
+
+
+class ConfigPluginBase(object):
+ """Base class for config plugins. Need to implement public interfaces."""
+ __metaclass__ = abc.ABCMeta
+
+ class InitializationFailure(Exception):
+ pass
+
+ def __init__(self, config):
+ if not config:
+ raise ConfigPluginBase.InitializationFailure(
+ 'Initialization parameters need to be assigned.')
+
+ self.config = config
+
+ @abc.abstractmethod
+ def get_config(self):
+ """Returns updated default configuration file."""
+
+ @abc.abstractmethod
+ def get_openstack_spec(self):
+ """Returns OpenStack specs for host."""
+
+ @abc.abstractmethod
+ def get_run_spec(self, openstack_spec):
+ """Returns RunSpec for given platform."""
+
+ @abc.abstractmethod
+ def validate_config(self, cfg):
+ """Validate config file."""
+
+ @abc.abstractmethod
+ def prepare_results_config(self, cfg):
+ """This function is called before running configuration is copied.
+ Example usage is to remove sensitive information like switch credentials.
+ """
+
+ @abc.abstractmethod
+ def get_version(self):
+ """Returns platform version."""
+
+
+class ConfigPlugin(ConfigPluginBase):
+ """No-op config plugin class. Does not change anything."""
+
+ def __init__(self, config):
+ ConfigPluginBase.__init__(self, config)
+
+ def get_config(self):
+ """Public interface for updating config file. Just returns given config."""
+ return self.config
+
+ def get_openstack_spec(self):
+ """Returns OpenStack specs for host."""
+ return specs.OpenStackSpec()
+
+ def get_run_spec(self, openstack_spec):
+ """Returns RunSpec for given platform."""
+ return specs.RunSpec(self.config.no_vswitch_access, openstack_spec)
+
+ def validate_config(self, config):
+ pass
+
+ def prepare_results_config(self, cfg):
+ return cfg
+
+ def get_version(self):
+ return {}
diff --git a/nfvbench/connection.py b/nfvbench/connection.py
new file mode 100644
index 0000000..0ef994f
--- /dev/null
+++ b/nfvbench/connection.py
@@ -0,0 +1,725 @@
+# Copyright 2013: Mirantis Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+"""High level ssh library.
+Usage examples:
+Execute command and get output:
+ ssh = sshclient.SSH('root', 'example.com', port=33)
+ status, stdout, stderr = ssh.execute('ps ax')
+ if status:
+ raise Exception('Command failed with non-zero status.')
+ print stdout.splitlines()
+Execute command with huge output:
+ class PseudoFile(object):
+ def write(chunk):
+ if 'error' in chunk:
+ email_admin(chunk)
+ ssh = sshclient.SSH('root', 'example.com')
+ ssh.run('tail -f /var/log/syslog', stdout=PseudoFile(), timeout=False)
+Execute local script on remote side:
+ ssh = sshclient.SSH('user', 'example.com')
+ status, out, err = ssh.execute('/bin/sh -s arg1 arg2',
+ stdin=open('~/myscript.sh', 'r'))
+Upload file:
+ ssh = sshclient.SSH('user', 'example.com')
+ ssh.run('cat > ~/upload/file.gz', stdin=open('/store/file.gz', 'rb'))
+Eventlet:
+ eventlet.monkey_patch(select=True, time=True)
+ or
+ eventlet.monkey_patch()
+ or
+ sshclient = eventlet.import_patched("opentstack.common.sshclient")
+"""
+
+import re
+import select
+import shlex
+import socket
+import StringIO
+import subprocess
+import sys
+import threading
+import time
+
+from log import LOG
+import paramiko
+
+# from rally.openstack.common.gettextutils import _
+
+
+class ConnectionError(Exception):
+ pass
+
+
+class Connection(object):
+
+ '''
+ A base connection class. Not intended to be constructed.
+ '''
+
+ def __init__(self):
+ self.distro_id = None
+ self.distro_id_like = None
+ self.distro_version = None
+ self.__get_distro()
+
+ def close(self):
+ pass
+
+ def execute(self, cmd, stdin=None, timeout=3600):
+ pass
+
+ def __extract_property(self, name, input_str):
+ expr = name + r'="?([\w\.]*)"?'
+ match = re.search(expr, input_str)
+ if match:
+ return match.group(1)
+ return 'Unknown'
+
+ # Get the linux distro
+ def __get_distro(self):
+ '''cat /etc/*-release | grep ID
+ Ubuntu:
+ DISTRIB_ID=Ubuntu
+ ID=ubuntu
+ ID_LIKE=debian
+ VERSION_ID="14.04"
+ RHEL:
+ ID="rhel"
+ ID_LIKE="fedora"
+ VERSION_ID="7.0"
+ '''
+ distro_cmd = "grep ID /etc/*-release"
+ (status, distro_out, _) = self.execute(distro_cmd)
+ if status:
+ distro_out = ''
+ self.distro_id = self.__extract_property('ID', distro_out)
+ self.distro_id_like = self.__extract_property('ID_LIKE', distro_out)
+ self.distro_version = self.__extract_property('VERSION_ID', distro_out)
+
+ def pidof(self, proc_name):
+ '''
+ Return a list containing the pids of all processes of a given name
+ the list is empty if there is no pid
+ '''
+ # the path update is necessary for RHEL
+ cmd = "PATH=$PATH:/usr/sbin pidof " + proc_name
+ (status, cmd_output, _) = self.execute(cmd)
+ if status:
+ return []
+ cmd_output = cmd_output.strip()
+ result = cmd_output.split()
+ return result
+
+ # kill pids in the given list of pids
+ def kill_proc(self, pid_list):
+ cmd = "kill -9 " + ' '.join(pid_list)
+ self.execute(cmd)
+
+ # check stats for a given path
+ def stat(self, path):
+ (status, cmd_output, _) = self.execute('stat ' + path)
+ if status:
+ return None
+ return cmd_output
+
+ def ping_check(self, target_ip, ping_count=2, pass_threshold=80):
+ '''helper function to ping from one host to an IP address,
+ for a given count and pass_threshold;
+ Steps:
+ ssh to the host and then ping to the target IP
+ then match the output and verify that the loss% is
+ less than the pass_threshold%
+ Return 1 if the criteria passes
+ Return 0, if it fails
+ '''
+ cmd = "ping -c " + str(ping_count) + " " + str(target_ip)
+ (_, cmd_output, _) = self.execute(cmd)
+
+ match = re.search(r'(\d*)% packet loss', cmd_output)
+ pkt_loss = match.group(1)
+ if int(pkt_loss) < int(pass_threshold):
+ return 1
+ else:
+ LOG.error('Ping to %s failed: %s', target_ip, cmd_output)
+ return 0
+
+ def read_remote_file(self, from_path):
+ '''
+ Read a remote file and save it to a buffer.
+ '''
+ cmd = "cat " + from_path
+ (status, cmd_output, _) = self.execute(cmd)
+ if status:
+ return None
+ return cmd_output
+
+ def get_host_os_version(self):
+ '''
+ Identify the host distribution/relase.
+ '''
+ os_release_file = "/etc/os-release"
+ sys_release_file = "/etc/system-release"
+ name = ""
+ version = ""
+
+ if self.stat(os_release_file):
+ data = self.read_remote_file(os_release_file)
+ if data is None:
+ LOG.error("Failed to read file %s", os_release_file)
+ return None
+
+ for line in data.splitlines():
+ mobj = re.match(r'NAME=(.*)', line)
+ if mobj:
+ name = mobj.group(1).strip("\"")
+
+ mobj = re.match(r'VERSION_ID=(.*)', line)
+ if mobj:
+ version = mobj.group(1).strip("\"")
+
+ os_name = name + " " + version
+ return os_name
+
+ if self.stat(sys_release_file):
+ data = self.read_remote_file(sys_release_file)
+ if data is None:
+ LOG.error("Failed to read file %s", sys_release_file)
+ return None
+
+ for line in data.splitlines():
+ mobj = re.match(r'Red Hat.*', line)
+ if mobj:
+ return mobj.group(0)
+
+ return None
+
+ def check_rpm_package_installed(self, rpm_pkg):
+ '''
+ Given a host and a package name, check if it is installed on the
+ system.
+ '''
+ check_pkg_cmd = "rpm -qa | grep " + rpm_pkg
+
+ (status, cmd_output, _) = self.execute(check_pkg_cmd)
+ if status:
+ return None
+
+ pkg_pattern = ".*" + rpm_pkg + ".*"
+ rpm_pattern = re.compile(pkg_pattern, re.IGNORECASE)
+
+ for line in cmd_output.splitlines():
+ mobj = rpm_pattern.match(line)
+ if mobj:
+ return mobj.group(0)
+
+ LOG.info("%s pkg installed ", rpm_pkg)
+
+ return None
+
+ def get_openstack_release(self, ver_str):
+ '''
+ Get the release series name from the package version
+ Refer to here for release tables:
+ https://wiki.openstack.org/wiki/Releases
+ '''
+ ver_table = {"2015.1": "Kilo",
+ "2014.2": "Juno",
+ "2014.1": "Icehouse",
+ "2013.2": "Havana",
+ "2013.1": "Grizzly",
+ "2012.2": "Folsom",
+ "2012.1": "Essex",
+ "2011.3": "Diablo",
+ "2011.2": "Cactus",
+ "2011.1": "Bexar",
+ "2010.1": "Austin"}
+
+ ver_prefix = re.search(r"20\d\d\.\d", ver_str).group(0)
+ if ver_prefix in ver_table:
+ return ver_table[ver_prefix]
+ else:
+ return "Unknown"
+
+ def check_openstack_version(self):
+ '''
+ Identify the openstack version running on the controller.
+ '''
+ nova_cmd = "nova-manage --version"
+ (status, _, err_output) = self.execute(nova_cmd)
+
+ if status:
+ return "Unknown"
+
+ ver_str = err_output.strip()
+ release_str = self.get_openstack_release(err_output)
+ return release_str + " (" + ver_str + ")"
+
+ def get_cpu_info(self):
+ '''
+ Get the CPU info of the controller.
+ Note: Here we are assuming the controller node has the exact
+ hardware as the compute nodes.
+ '''
+
+ cmd = 'cat /proc/cpuinfo | grep -m1 "model name"'
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ model_name = re.search(r":\s(.*)", std_output).group(1)
+
+ cmd = 'cat /proc/cpuinfo | grep "model name" | wc -l'
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ cores = std_output.strip()
+
+ return (cores + " * " + model_name)
+
+ def get_nic_name(self, agent_type, encap, internal_iface_dict):
+ '''
+ Get the NIC info of the controller.
+ Note: Here we are assuming the controller node has the exact
+ hardware as the compute nodes.
+ '''
+
+ # The internal_ifac_dict is a dictionary contains the mapping between
+ # hostname and the internal interface name like below:
+ # {u'hh23-4': u'eth1', u'hh23-5': u'eth1', u'hh23-6': u'eth1'}
+
+ cmd = "hostname"
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ hostname = std_output.strip()
+
+ if hostname in internal_iface_dict:
+ iface = internal_iface_dict[hostname]
+ else:
+ return "Unknown"
+
+ # Figure out which interface is for internal traffic
+ if 'Linux bridge' in agent_type:
+ ifname = iface
+ elif 'Open vSwitch' in agent_type:
+ if encap == 'vlan':
+ # [root@hh23-10 ~]# ovs-vsctl list-ports br-inst
+ # eth1
+ # phy-br-inst
+ cmd = 'ovs-vsctl list-ports ' + \
+ iface + ' | grep -E "^[^phy].*"'
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ ifname = std_output.strip()
+ elif encap == 'vxlan' or encap == 'gre':
+ # This is complicated. We need to first get the local IP address on
+ # br-tun, then do a reverse lookup to get the physical interface.
+ #
+ # [root@hh23-4 ~]# ip addr show to "23.23.2.14"
+ # 3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
+ # inet 23.23.2.14/24 brd 23.23.2.255 scope global eth1
+ # valid_lft forever preferred_lft forever
+ cmd = "ip addr show to " + iface + " | awk -F: '{print $2}'"
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ ifname = std_output.strip()
+ else:
+ return "Unknown"
+
+ cmd = 'ethtool -i ' + ifname + ' | grep bus-info'
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ bus_info = re.search(r":\s(.*)", std_output).group(1)
+
+ cmd = 'lspci -s ' + bus_info
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+ nic_name = re.search(
+ r"Ethernet controller:\s(.*)",
+ std_output).group(1)
+
+ return (nic_name)
+
+ def get_l2agent_version(self, agent_type):
+ '''
+ Get the L2 agent version of the controller.
+ Note: Here we are assuming the controller node has the exact
+ hardware as the compute nodes.
+ '''
+ if 'Linux bridge' in agent_type:
+ cmd = "brctl --version | awk -F',' '{print $2}'"
+ ver_string = "Linux Bridge "
+ elif 'Open vSwitch' in agent_type:
+ cmd = "ovs-vsctl --version | awk -F')' '{print $2}'"
+ ver_string = "OVS "
+ else:
+ return "Unknown"
+
+ (status, std_output, _) = self.execute(cmd)
+ if status:
+ return "Unknown"
+
+ return ver_string + std_output.strip()
+
+
+class SSHError(Exception):
+ pass
+
+
+class SSHTimeout(SSHError):
+ pass
+
+# Check IPv4 address syntax - not completely fool proof but will catch
+# some invalid formats
+
+
+def is_ipv4(address):
+ try:
+ socket.inet_aton(address)
+ except socket.error:
+ return False
+ return True
+
+
+class SSHAccess(object):
+
+ '''
+ A class to contain all the information needed to access a host
+ (native or virtual) using SSH
+ '''
+
+ def __init__(self, arg_value=None):
+ '''
+ decode user@host[:pwd]
+ 'hugo@1.1.1.1:secret' -> ('hugo', '1.1.1.1', 'secret', None)
+ 'huggy@2.2.2.2' -> ('huggy', '2.2.2.2', None, None)
+ None ->(None, None, None, None)
+ Examples of fatal errors (will call exit):
+ 'hutch@q.1.1.1' (invalid IP)
+ '@3.3.3.3' (missing username)
+ 'hiro@' or 'buggy' (missing host IP)
+ The error field will be None in case of success or will
+ contain a string describing the error
+ '''
+ self.username = None
+ self.host = None
+ self.password = None
+ # name of the file that contains the private key
+ self.private_key_file = None
+ # this is the private key itself (a long string starting with
+ # -----BEGIN RSA PRIVATE KEY-----
+ # used when the private key is not saved in any file
+ self.private_key = None
+ self.public_key_file = None
+ self.port = 22
+ self.error = None
+
+ if not arg_value:
+ return
+ match = re.search(r'^([^@]+)@([0-9\.]+):?(.*)$', arg_value)
+ if not match:
+ self.error = 'Invalid argument: ' + arg_value
+ return
+ if not is_ipv4(match.group(2)):
+ self.error = 'Invalid IPv4 address ' + match.group(2)
+ return
+ (self.username, self.host, self.password) = match.groups()
+
+ def copy_from(self, ssh_access):
+ self.username = ssh_access.username
+ self.host = ssh_access.host
+ self.port = ssh_access.port
+ self.password = ssh_access.password
+ self.private_key = ssh_access.private_key
+ self.public_key_file = ssh_access.public_key_file
+ self.private_key_file = ssh_access.private_key_file
+
+
+class SSH(Connection):
+
+ """Represent ssh connection."""
+
+ def __init__(self, ssh_access,
+ connect_timeout=60,
+ connect_retry_count=30,
+ connect_retry_wait_sec=2):
+ """Initialize SSH client.
+ :param user: ssh username
+ :param host: hostname or ip address of remote ssh server
+ :param port: remote ssh port
+ :param pkey: RSA or DSS private key string or file object
+ :param key_filename: private key filename
+ :param password: password
+ :param connect_timeout: timeout when connecting ssh
+ :param connect_retry_count: how many times to retry connecting
+ :param connect_retry_wait_sec: seconds to wait between retries
+ """
+
+ self.ssh_access = ssh_access
+ if ssh_access.private_key:
+ self.pkey = self._get_pkey(ssh_access.private_key)
+ else:
+ self.pkey = None
+ self._client = False
+ self.connect_timeout = connect_timeout
+ self.connect_retry_count = connect_retry_count
+ self.connect_retry_wait_sec = connect_retry_wait_sec
+ super(SSH, self).__init__()
+
+ def _get_pkey(self, key):
+ '''Get the binary form of the private key
+ from the text form
+ '''
+ if isinstance(key, basestring):
+ key = StringIO.StringIO(key)
+ errors = []
+ for key_class in (paramiko.rsakey.RSAKey, paramiko.dsskey.DSSKey):
+ try:
+ return key_class.from_private_key(key)
+ except paramiko.SSHException as exc:
+ errors.append(exc)
+ raise SSHError('Invalid pkey: %s' % (errors))
+
+ def _is_active(self):
+ if self._client:
+ try:
+ transport = self._client.get_transport()
+ session = transport.open_session()
+ session.close()
+ return True
+ except Exception:
+ return False
+ else:
+ return False
+
+ def _get_client(self, force=False):
+ if not force and self._is_active():
+ return self._client
+ if self._client:
+ LOG.info('Re-establishing ssh connection with %s' % (self.ssh_access.host))
+ self._client.close()
+ self._client = paramiko.SSHClient()
+ self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ for _ in range(self.connect_retry_count):
+ try:
+ self._client.connect(self.ssh_access.host,
+ username=self.ssh_access.username,
+ port=self.ssh_access.port,
+ pkey=self.pkey,
+ key_filename=self.ssh_access.private_key_file,
+ password=self.ssh_access.password,
+ timeout=self.connect_timeout)
+ self._client.get_transport().set_keepalive(5)
+ return self._client
+ except (paramiko.AuthenticationException,
+ paramiko.BadHostKeyException,
+ paramiko.SSHException,
+ socket.error,
+ Exception):
+ time.sleep(self.connect_retry_wait_sec)
+
+ self._client = None
+ msg = '[%s] SSH Connection failed after %s attempts' % (self.ssh_access.host,
+ self.connect_retry_count)
+ raise SSHError(msg)
+
+ def _get_session(self):
+ client = self._get_client()
+ for _ in range(self.connect_retry_count):
+ try:
+ transport = client.get_transport()
+ session = transport.open_session()
+ return session
+ except Exception:
+ client = self._get_client(force=True)
+ return None
+
+ def close(self):
+ super(SSH, self).close()
+ if self._client:
+ self._client.close()
+ self._client = False
+
+ def run(self, cmd, stdin=None, stdout=None, stderr=None,
+ raise_on_error=True, timeout=3600, sudo=False):
+ """Execute specified command on the server.
+ :param cmd: Command to be executed.
+ :param stdin: Open file or string to pass to stdin.
+ :param stdout: Open file to connect to stdout.
+ :param stderr: Open file to connect to stderr.
+ :param raise_on_error: If False then exit code will be return. If True
+ then exception will be raized if non-zero code.
+ :param timeout: Timeout in seconds for command execution.
+ Default 1 hour. No timeout if set to 0.
+ :param sudo: Executes command as sudo with default password
+ """
+
+ if isinstance(stdin, basestring):
+ stdin = StringIO.StringIO(stdin)
+
+ return self._run(cmd, stdin=stdin, stdout=stdout,
+ stderr=stderr, raise_on_error=raise_on_error,
+ timeout=timeout, sudo=sudo)
+
+ def _run(self, cmd, stdin=None, stdout=None, stderr=None,
+ raise_on_error=True, timeout=3600, sudo=False):
+
+ session = self._get_session()
+
+ if session is None:
+ raise SSHError('Unable to open session to ssh connection')
+
+ if sudo:
+ cmd = "echo " + self.ssh_access.password + " | sudo -S -p '' " + cmd
+ session.get_pty()
+
+ session.exec_command(cmd)
+ start_time = time.time()
+
+ data_to_send = ''
+ stderr_data = None
+
+ # If we have data to be sent to stdin then `select' should also
+ # check for stdin availability.
+ if stdin and not stdin.closed:
+ writes = [session]
+ else:
+ writes = []
+
+ while True:
+ # Block until data can be read/write.
+ select.select([session], writes, [session], 1)
+
+ if session.recv_ready():
+ data = session.recv(4096)
+ if stdout is not None:
+ stdout.write(data)
+ continue
+
+ if session.recv_stderr_ready():
+ stderr_data = session.recv_stderr(4096)
+ if stderr is not None:
+ stderr.write(stderr_data)
+ continue
+
+ if session.send_ready():
+ if stdin is not None and not stdin.closed:
+ if not data_to_send:
+ data_to_send = stdin.read(4096)
+ if not data_to_send:
+ stdin.close()
+ session.shutdown_write()
+ writes = []
+ continue
+ sent_bytes = session.send(data_to_send)
+ data_to_send = data_to_send[sent_bytes:]
+
+ if session.exit_status_ready():
+ break
+
+ if timeout and (time.time() - timeout) > start_time:
+ args = {'cmd': cmd, 'host': self.ssh_access.host}
+ raise SSHTimeout(('Timeout executing command '
+ '"%(cmd)s" on host %(host)s') % args)
+ # if e:
+ # raise SSHError('Socket error.')
+
+ exit_status = session.recv_exit_status()
+ if 0 != exit_status and raise_on_error:
+ fmt = ('Command "%(cmd)s" failed with exit_status %(status)d.')
+ details = fmt % {'cmd': cmd, 'status': exit_status}
+ if stderr_data:
+ details += (' Last stderr data: "%s".') % stderr_data
+ raise SSHError(details)
+ return exit_status
+
+ def execute(self, cmd, stdin=None, timeout=3600, sudo=False):
+ """Execute the specified command on the server.
+ :param cmd: Command to be executed.
+ :param stdin: Open file to be sent on process stdin.
+ :param timeout: Timeout for execution of the command.
+ Return tuple (exit_status, stdout, stderr)
+ """
+ stdout = StringIO.StringIO()
+ stderr = StringIO.StringIO()
+
+ exit_status = self.run(cmd, stderr=stderr,
+ stdout=stdout, stdin=stdin,
+ timeout=timeout, raise_on_error=False, sudo=sudo)
+ stdout.seek(0)
+ stderr.seek(0)
+ return (exit_status, stdout.read(), stderr.read())
+
+ def wait(self, timeout=120, interval=1):
+ """Wait for the host will be available via ssh."""
+ start_time = time.time()
+ while True:
+ try:
+ return self.execute('uname')
+ except (socket.error, SSHError):
+ time.sleep(interval)
+ if time.time() > (start_time + timeout):
+ raise SSHTimeout(
+ ('Timeout waiting for "%s"') %
+ self.ssh_access.host)
+
+
+class SubprocessTimeout(Exception):
+ pass
+
+
+class Subprocess(Connection):
+
+ """Represent subprocess connection."""
+
+ def execute(self, cmd, stdin=None, timeout=3600):
+ process = subprocess.Popen(shlex.split(cmd), stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ shell=True)
+ timer = threading.Timer(timeout, process.kill)
+ stdout, stderr = process.communicate(input=stdin)
+ status = process.wait()
+ if timer.is_alive():
+ timer.cancel()
+ raise SubprocessTimeout('Timeout executing command "%(cmd)s"')
+ return (status, stdout, stderr)
+
+
+##################################################
+# Only invoke the module directly for test purposes. Should be
+# invoked from pns script.
+##################################################
+def main():
+ # As argument pass the SSH access string, e.g. "localadmin@1.1.1.1:secret"
+ test_ssh = SSH(SSHAccess(sys.argv[1]))
+
+ print 'ID=' + test_ssh.distro_id
+ print 'ID_LIKE=' + test_ssh.distro_id_like
+ print 'VERSION_ID=' + test_ssh.distro_version
+
+ # ssh.wait()
+ # print ssh.pidof('bash')
+ # print ssh.stat('/tmp')
+ print test_ssh.check_openstack_version()
+ print test_ssh.get_cpu_info()
+ print test_ssh.get_l2agent_version("Open vSwitch agent")
+
+if __name__ == "__main__":
+ main()
diff --git a/nfvbench/credentials.py b/nfvbench/credentials.py
new file mode 100644
index 0000000..0c8470e
--- /dev/null
+++ b/nfvbench/credentials.py
@@ -0,0 +1,166 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+# Module for credentials in Openstack
+import getpass
+from keystoneauth1.identity import v2
+from keystoneauth1.identity import v3
+from keystoneauth1 import session
+import os
+import re
+
+from log import LOG
+
+
+class Credentials(object):
+
+ def get_session(self):
+ dct = {
+ 'username': self.rc_username,
+ 'password': self.rc_password,
+ 'auth_url': self.rc_auth_url
+ }
+ auth = None
+
+ if self.rc_identity_api_version == 3:
+ dct.update({
+ 'project_name': self.rc_project_name,
+ 'project_domain_name': self.rc_project_domain_name,
+ 'user_domain_name': self.rc_user_domain_name
+ })
+ auth = v3.Password(**dct)
+ else:
+ dct.update({
+ 'tenant_name': self.rc_tenant_name
+ })
+ auth = v2.Password(**dct)
+ return session.Session(auth=auth, verify=self.rc_cacert)
+
+ def __parse_openrc(self, file):
+ export_re = re.compile('export OS_([A-Z_]*)="?(.*)')
+ for line in file:
+ line = line.strip()
+ mstr = export_re.match(line)
+ if mstr:
+ # get rif of posible trailing double quote
+ # the first one was removed by the re
+ name = mstr.group(1)
+ value = mstr.group(2)
+ if value.endswith('"'):
+ value = value[:-1]
+ # get rid of password assignment
+ # echo "Please enter your OpenStack Password: "
+ # read -sr OS_PASSWORD_INPUT
+ # export OS_PASSWORD=$OS_PASSWORD_INPUT
+ if value.startswith('$'):
+ continue
+ # Check if api version is provided
+ # Default is keystone v2
+ if name == 'IDENTITY_API_VERSION':
+ self.rc_identity_api_version = int(value)
+
+ # now match against wanted variable names
+ elif name == 'USERNAME':
+ self.rc_username = value
+ elif name == 'AUTH_URL':
+ self.rc_auth_url = value
+ elif name == 'TENANT_NAME':
+ self.rc_tenant_name = value
+ elif name == "CACERT":
+ self.rc_cacert = value
+ elif name == "REGION_NAME":
+ self.rc_region_name = value
+ elif name == "PASSWORD":
+ self.rc_password = value
+ elif name == "USER_DOMAIN_NAME":
+ self.rc_user_domain_name = value
+ elif name == "PROJECT_NAME":
+ self.rc_project_name = value
+ elif name == "PROJECT_DOMAIN_NAME":
+ self.rc_project_domain_name = value
+
+ #
+ # Read a openrc file and take care of the password
+ # The 2 args are passed from the command line and can be None
+ #
+ def __init__(self, openrc_file, pwd=None, no_env=False):
+ self.rc_password = None
+ self.rc_username = None
+ self.rc_tenant_name = None
+ self.rc_auth_url = None
+ self.rc_cacert = None
+ self.rc_region_name = None
+ self.rc_user_domain_name = None
+ self.rc_project_domain_name = None
+ self.rc_project_name = None
+ self.rc_identity_api_version = 2
+ success = True
+
+ if openrc_file:
+ if isinstance(openrc_file, str):
+ if os.path.exists(openrc_file):
+ self.__parse_openrc(open(openrc_file))
+ else:
+ LOG.error('Error: rc file does not exist %s', openrc_file)
+ success = False
+ else:
+ self.__parse_openrc(openrc_file)
+ elif not no_env:
+ # no openrc file passed - we assume the variables have been
+ # sourced by the calling shell
+ # just check that they are present
+ if 'OS_IDENTITY_API_VERSION' in os.environ:
+ self.rc_identity_api_version = int(os.environ['OS_IDENTITY_API_VERSION'])
+
+ if self.rc_identity_api_version == 2:
+ for varname in ['OS_USERNAME', 'OS_AUTH_URL', 'OS_TENANT_NAME']:
+ if varname not in os.environ:
+ LOG.warning('%s is missing', varname)
+ success = False
+ if success:
+ self.rc_username = os.environ['OS_USERNAME']
+ self.rc_auth_url = os.environ['OS_AUTH_URL']
+ self.rc_tenant_name = os.environ['OS_TENANT_NAME']
+ if 'OS_REGION_NAME' in os.environ:
+ self.rc_region_name = os.environ['OS_REGION_NAME']
+ elif self.rc_identity_api_version == 3:
+ for varname in ['OS_USERNAME', 'OS_AUTH_URL', 'OS_PROJECT_NAME',
+ 'OS_PROJECT_DOMAIN_NAME', 'OS_USER_DOMAIN_NAME']:
+ if varname not in os.environ:
+ LOG.warning('%s is missing', varname)
+ success = False
+ if success:
+ self.rc_username = os.environ['OS_USERNAME']
+ self.rc_auth_url = os.environ['OS_AUTH_URL']
+ self.rc_project_name = os.environ['OS_PROJECT_NAME']
+ self.rc_project_domain_id = os.environ['OS_PROJECT_DOMAIN_NAME']
+ self.rc_user_domain_id = os.environ['OS_USER_DOMAIN_NAME']
+ if 'OS_CACERT' in os.environ:
+ self.rc_cacert = os.environ['OS_CACERT']
+
+
+ # always override with CLI argument if provided
+ if pwd:
+ self.rc_password = pwd
+ # if password not know, check from env variable
+ elif self.rc_auth_url and not self.rc_password and success:
+ if 'OS_PASSWORD' in os.environ and not no_env:
+ self.rc_password = os.environ['OS_PASSWORD']
+ else:
+ # interactively ask for password
+ self.rc_password = getpass.getpass(
+ 'Please enter your OpenStack Password: ')
+ if not self.rc_password:
+ self.rc_password = ""
diff --git a/nfvbench/factory.py b/nfvbench/factory.py
new file mode 100644
index 0000000..35a8c1b
--- /dev/null
+++ b/nfvbench/factory.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from chain_clients import EXTStageClient
+from chain_clients import PVPStageClient
+from chain_clients import PVVPStageClient
+from chain_managers import EXTStatsManager
+from chain_managers import PVPStatsManager
+from chain_managers import PVVPStatsManager
+import chain_workers as workers
+from config_plugin import ConfigPlugin
+from specs import ChainType
+import tor_client
+
+
+class BasicFactory(object):
+
+ chain_classes = [ChainType.EXT, ChainType.PVP, ChainType.PVVP]
+
+ chain_stats_classes = {
+ ChainType.EXT: EXTStatsManager,
+ ChainType.PVP: PVPStatsManager,
+ ChainType.PVVP: PVVPStatsManager,
+ }
+
+ stage_clients_classes = {
+ ChainType.EXT: EXTStageClient,
+ ChainType.PVP: PVPStageClient,
+ ChainType.PVVP: PVVPStageClient,
+ }
+
+ def get_stats_class(self, service_chain):
+ CLASS = self.chain_stats_classes.get(service_chain, None)
+ if CLASS is None:
+ raise Exception("Service chain '{}' not supported.".format(service_chain))
+
+ return CLASS
+
+ def get_stage_class(self, service_chain):
+ CLASS = self.stage_clients_classes.get(service_chain, None)
+ if CLASS is None:
+ raise Exception("VM Client for chain '{}' not supported.".format(service_chain))
+
+ return CLASS
+
+ def get_chain_worker(self, encaps, service_chain):
+ return workers.BasicWorker
+
+ def get_tor_class(self, tor_type, no_tor_access):
+ if no_tor_access or not tor_type:
+ # if no TOR access is required, use basic no-op client
+ tor_type = 'BasicTORClient'
+
+ return getattr(tor_client, tor_type)
+
+ def get_config_plugin_class(self):
+ return ConfigPlugin
diff --git a/nfvbench/log.py b/nfvbench/log.py
new file mode 100644
index 0000000..22afefe
--- /dev/null
+++ b/nfvbench/log.py
@@ -0,0 +1,40 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+
+def setup(product_name):
+ # logging.basicConfig()
+ formatter_str = '%(asctime)s %(levelname)s %(message)s'
+ handler = logging.StreamHandler()
+ handler.setFormatter(logging.Formatter(formatter_str))
+
+ # Add handler to logger
+ logger = logging.getLogger(product_name)
+ logger.addHandler(handler)
+
+
+def set_level(product, debug=False):
+ log_level = logging.DEBUG if debug else logging.INFO
+ logger = logging.getLogger(product)
+ logger.setLevel(log_level)
+
+
+def getLogger(product):
+ logger = logging.getLogger(product)
+
+ return logger
+
+LOG = getLogger('nfvbench')
diff --git a/nfvbench/network.py b/nfvbench/network.py
new file mode 100644
index 0000000..e097c2b
--- /dev/null
+++ b/nfvbench/network.py
@@ -0,0 +1,62 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+
+class Interface(object):
+
+ def __init__(self, name, device, tx_packets, rx_packets):
+ self.name = name
+ self.device = device
+ self.packets = {
+ 'tx': tx_packets,
+ 'rx': rx_packets
+ }
+
+ def set_packets(self, tx, rx):
+ self.packets = {
+ 'tx': tx,
+ 'rx': rx
+ }
+
+ def set_packets_diff(self, tx, rx):
+ self.packets = {
+ 'tx': tx - self.packets['tx'],
+ 'rx': rx - self.packets['rx'],
+ }
+
+ def is_no_op(self):
+ return self.name is None
+
+ def get_packet_count(self, traffic_type):
+ return self.packets.get(traffic_type, 0)
+
+ @staticmethod
+ def no_op():
+ return Interface(None, None, 0, 0)
+
+
+class Network(object):
+
+ def __init__(self, interfaces=None, reverse=False):
+ if interfaces is None:
+ interfaces = []
+ self.interfaces = interfaces
+ self.reverse = reverse
+
+ def add_interface(self, interface):
+ self.interfaces.append(interface)
+
+ def get_interfaces(self):
+ return self.interfaces[::-1] if self.reverse else self.interfaces
diff --git a/nfvbench/nfvbench.py b/nfvbench/nfvbench.py
new file mode 100644
index 0000000..0dcf2f1
--- /dev/null
+++ b/nfvbench/nfvbench.py
@@ -0,0 +1,491 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from __init__ import __version__
+import argparse
+from attrdict import AttrDict
+from chain_runner import ChainRunner
+from collections import defaultdict
+from config import config_load
+from config import config_loads
+import credentials
+import datetime
+from factory import BasicFactory
+import importlib
+import json
+import log
+from log import LOG
+from nfvbenchd import WebSocketIoServer
+import os
+import pbr.version
+from pkg_resources import resource_string
+from specs import Specs
+from summarizer import NFVBenchSummarizer
+import sys
+import traceback
+from traffic_client import TrafficGeneratorFactory
+import utils
+
+
+class NFVBench(object):
+ """Main class of NFV benchmarking tool."""
+ STATUS_OK = 'OK'
+ STATUS_ERROR = 'ERROR'
+
+ def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
+ self.base_config = config
+ self.config = None
+ self.config_plugin = config_plugin
+ self.factory = factory
+ self.notifier = notifier
+ self.cred = credentials.Credentials(config.openrc_file, None, False)
+ self.chain_runner = None
+ self.specs = Specs()
+ self.specs.set_openstack_spec(openstack_spec)
+ self.clients = defaultdict(lambda: None)
+ self.vni_ports = []
+ sys.stdout.flush()
+
+ def setup(self):
+ self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
+ self.chain_runner = ChainRunner(self.config,
+ self.clients,
+ self.cred,
+ self.specs,
+ self.factory,
+ self.notifier)
+
+ def set_notifier(self, notifier):
+ self.notifier = notifier
+
+ def run(self, opts):
+ status = NFVBench.STATUS_OK
+ result = None
+ message = ''
+ try:
+ self.update_config(opts)
+ self.setup()
+
+ result = {
+ "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "nfvbench_version": __version__,
+ "openstack_spec": {
+ "vswitch": self.specs.openstack.vswitch,
+ "encaps": self.specs.openstack.encaps
+ },
+ "config": self.config_plugin.prepare_results_config(dict(self.config)),
+ "benchmarks": {
+ "network": {
+ "service_chain": self.chain_runner.run(),
+ "versions": self.chain_runner.get_version(),
+ }
+ }
+ }
+ result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
+ except Exception:
+ status = NFVBench.STATUS_ERROR
+ message = traceback.format_exc()
+ except KeyboardInterrupt:
+ status = NFVBench.STATUS_ERROR
+ message = traceback.format_exc()
+ finally:
+ if self.chain_runner:
+ self.chain_runner.close()
+
+ if status == NFVBench.STATUS_OK:
+ result = utils.dict_to_json_dict(result)
+ return {
+ 'status': status,
+ 'result': result
+ }
+ else:
+ return {
+ 'status': status,
+ 'error_message': message
+ }
+
+ def print_summary(self, result):
+ """Print summary of the result"""
+ print NFVBenchSummarizer(result)
+ sys.stdout.flush()
+
+ def save(self, result):
+ """Save results in json format file."""
+ utils.save_json_result(result,
+ self.config.json_file,
+ self.config.std_json_path,
+ self.config.service_chain,
+ self.config.service_chain_count,
+ self.config.flow_count,
+ self.config.frame_sizes)
+
+ def update_config(self, opts):
+ self.config = AttrDict(dict(self.base_config))
+ self.config.update(opts)
+
+ self.config.service_chain = self.config.service_chain.upper()
+ self.config.service_chain_count = int(self.config.service_chain_count)
+ self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
+ required_flow_count = self.config.service_chain_count * 2
+ if self.config.flow_count < required_flow_count:
+ LOG.info("Flow count '{}' has been set to minimum value of '{}' "
+ "for current configuration".format(self.config.flow_count,
+ required_flow_count))
+ self.config.flow_count = required_flow_count
+
+ if self.config.flow_count % 2 != 0:
+ self.config.flow_count += 1
+
+ self.config.duration_sec = float(self.config.duration_sec)
+ self.config.interval_sec = float(self.config.interval_sec)
+
+ # Get traffic generator profile config
+ if not self.config.generator_profile:
+ self.config.generator_profile = self.config.traffic_generator.default_profile
+
+ generator_factory = TrafficGeneratorFactory(self.config)
+ self.config.generator_config = \
+ generator_factory.get_generator_config(self.config.generator_profile)
+
+ if not any(self.config.generator_config.pcis):
+ raise Exception("PCI addresses configuration for selected traffic generator profile "
+ "({tg_profile}) are missing. Please specify them in configuration file."
+ .format(tg_profile=self.config.generator_profile))
+
+ if self.config.traffic is None or len(self.config.traffic) == 0:
+ raise Exception("No traffic profile found in traffic configuration, "
+ "please fill 'traffic' section in configuration file.")
+
+ if isinstance(self.config.traffic, tuple):
+ self.config.traffic = self.config.traffic[0]
+
+ self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
+
+ self.config.ipv6_mode = False
+ self.config.no_dhcp = True
+ self.config.same_network_only = True
+ if self.config.openrc_file:
+ self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
+
+ self.config.ndr_run = (not self.config.no_traffic
+ and 'ndr' in self.config.rate.strip().lower().split('_'))
+ self.config.pdr_run = (not self.config.no_traffic
+ and 'pdr' in self.config.rate.strip().lower().split('_'))
+ self.config.single_run = (not self.config.no_traffic
+ and not (self.config.ndr_run or self.config.pdr_run))
+
+ if self.config.vlans and len(self.config.vlans) != 2:
+ raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
+
+ self.config.json_file = self.config.json if self.config.json else None
+ if self.config.json_file:
+ (path, filename) = os.path.split(self.config.json)
+ if not os.path.exists(path):
+ raise Exception('Please provide existing path for storing results in JSON file. '
+ 'Path used: {path}'.format(path=path))
+
+ self.config.std_json_path = self.config.std_json if self.config.std_json else None
+ if self.config.std_json_path:
+ if not os.path.exists(self.config.std_json):
+ raise Exception('Please provide existing path for storing results in JSON file. '
+ 'Path used: {path}'.format(path=self.config.std_json_path))
+
+ self.config_plugin.validate_config(self.config)
+
+
+def parse_opts_from_cli():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-c', '--config', dest='config',
+ action='store',
+ help='Override default values with a config file or '
+ 'a yaml/json config string',
+ metavar='<file_name_or_yaml>')
+
+ parser.add_argument('--server', dest='server',
+ default=None,
+ action='store',
+ metavar='<http_root_pathname>',
+ help='Run nfvbench in server mode and pass'
+ ' the HTTP root folder full pathname')
+
+ parser.add_argument('--host', dest='host',
+ action='store',
+ default='0.0.0.0',
+ help='Host IP address on which server will be listening (default 0.0.0.0)')
+
+ parser.add_argument('-p', '--port', dest='port',
+ action='store',
+ default=7555,
+ help='Port on which server will be listening (default 7555)')
+
+ parser.add_argument('-sc', '--service-chain', dest='service_chain',
+ choices=BasicFactory.chain_classes,
+ action='store',
+ help='Service chain to run')
+
+ parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
+ action='store',
+ help='Set number of service chains to run',
+ metavar='<service_chain_count>')
+
+ parser.add_argument('-fc', '--flow-count', dest='flow_count',
+ action='store',
+ help='Set number of total flows for all chains and all directions',
+ metavar='<flow_count>')
+
+ parser.add_argument('--rate', dest='rate',
+ action='store',
+ help='Specify rate in pps, bps or %% as total for all directions',
+ metavar='<rate>')
+
+ parser.add_argument('--duration', dest='duration_sec',
+ action='store',
+ help='Set duration to run traffic generator (in seconds)',
+ metavar='<duration_sec>')
+
+ parser.add_argument('--interval', dest='interval_sec',
+ action='store',
+ help='Set interval to record traffic generator stats (in seconds)',
+ metavar='<interval_sec>')
+
+ parser.add_argument('--inter-node', dest='inter_node',
+ default=None,
+ action='store_true',
+ help='run VMs in different compute nodes (PVVP only)')
+
+ parser.add_argument('--sriov', dest='sriov',
+ default=None,
+ action='store_true',
+ help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
+
+ parser.add_argument('-d', '--debug', dest='debug',
+ action='store_true',
+ default=None,
+ help='print debug messages (verbose)')
+
+ parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
+ action='store',
+ help='Traffic generator profile to use')
+
+ parser.add_argument('-i', '--image', dest='image_name',
+ action='store',
+ help='VM image name to use')
+
+ parser.add_argument('-0', '--no-traffic', dest='no_traffic',
+ default=None,
+ action='store_true',
+ help='Check config and connectivity only - do not generate traffic')
+
+ parser.add_argument('--no-arp', dest='no_arp',
+ default=None,
+ action='store_true',
+ help='Do not use ARP to find MAC addresses, '
+ 'instead use values in config file')
+
+ parser.add_argument('--no-reset', dest='no_reset',
+ default=None,
+ action='store_true',
+ help='Do not reset counters prior to running')
+
+ parser.add_argument('--no-int-config', dest='no_int_config',
+ default=None,
+ action='store_true',
+ help='Skip interfaces config on EXT service chain')
+
+ parser.add_argument('--no-tor-access', dest='no_tor_access',
+ default=None,
+ action='store_true',
+ help='Skip TOR switch configuration and retrieving of stats')
+
+ parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
+ default=None,
+ action='store_true',
+ help='Skip vswitch configuration and retrieving of stats')
+
+ parser.add_argument('--no-cleanup', dest='no_cleanup',
+ default=None,
+ action='store_true',
+ help='no cleanup after run')
+
+ parser.add_argument('--json', dest='json',
+ action='store',
+ help='store results in json format file',
+ metavar='<path>/<filename>')
+
+ parser.add_argument('--std-json', dest='std_json',
+ action='store',
+ help='store results in json format file with nfvbench standard filename: '
+ '<service-chain-type>-<service-chain-count>-<flow-count>'
+ '-<packet-sizes>.json',
+ metavar='<path>')
+
+ parser.add_argument('--show-default-config', dest='show_default_config',
+ default=None,
+ action='store_true',
+ help='print the default config in yaml format (unedited)')
+
+ parser.add_argument('--show-config', dest='show_config',
+ default=None,
+ action='store_true',
+ help='print the running config in json format')
+
+ parser.add_argument('-ss', '--show-summary', dest='summary',
+ action='store',
+ help='Show summary from nfvbench json file',
+ metavar='<json>')
+
+ parser.add_argument('-v', '--version', dest='version',
+ default=None,
+ action='store_true',
+ help='Show version')
+
+ parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
+ action='append',
+ help='Override traffic profile frame sizes',
+ metavar='<frame_size_bytes or IMIX>')
+
+ parser.add_argument('--unidir', dest='unidir',
+ action='store_true',
+ default=None,
+ help='Override traffic profile direction (requires -fs)')
+
+ opts, unknown_opts = parser.parse_known_args()
+ return opts, unknown_opts
+
+
+def load_default_config():
+ default_cfg = resource_string(__name__, "cfg.default.yaml")
+ config = config_loads(default_cfg)
+ config.name = '(built-in default config)'
+ return config, default_cfg
+
+
+def override_custom_traffic(config, frame_sizes, unidir):
+ """Override the traffic profiles with a custom one
+ """
+ if frame_sizes is not None:
+ traffic_profile_name = "custom_traffic_profile"
+ config.traffic_profile = [
+ {
+ "l2frame_size": frame_sizes,
+ "name": traffic_profile_name
+ }
+ ]
+ else:
+ traffic_profile_name = config.traffic["profile"]
+
+ bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
+ config.traffic = {
+ "bidirectional": bidirectional,
+ "profile": traffic_profile_name
+ }
+
+
+def main():
+ try:
+ log.setup('nfvbench')
+ # load default config file
+ config, default_cfg = load_default_config()
+ # 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']))
+ # create config plugin for this platform
+ config_plugin = factory.get_config_plugin_class()(config)
+ config = config_plugin.get_config()
+ openstack_spec = config_plugin.get_openstack_spec()
+
+ opts, unknown_opts = parse_opts_from_cli()
+ log.set_level('nfvbench', debug=opts.debug)
+
+ if opts.version:
+ print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
+ sys.exit(0)
+
+ if opts.summary:
+ with open(opts.summary) as json_data:
+ print NFVBenchSummarizer(json.load(json_data))
+ sys.exit(0)
+
+ # show default config in text/yaml format
+ if opts.show_default_config:
+ print default_cfg
+ sys.exit(0)
+
+ config.name = ''
+ if opts.config:
+ # 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):
+ print opts.config
+ config = config_load(opts.config, config)
+ config.name = os.path.basename(opts.config)
+ else:
+ config = config_loads(opts.config, config)
+
+ # traffic profile override options
+ override_custom_traffic(config, opts.frame_sizes, opts.unidir)
+
+ # copy over cli options that are used in config
+ config.generator_profile = opts.generator_profile
+
+ # show running config in json format
+ if opts.show_config:
+ print json.dumps(config, sort_keys=True, indent=4)
+ sys.exit(0)
+
+ nfvbench = NFVBench(config, openstack_spec, config_plugin, factory)
+
+ if opts.server:
+ if os.path.isdir(opts.server):
+ server = WebSocketIoServer(opts.server, nfvbench)
+ nfvbench.set_notifier(server)
+ try:
+ port = int(opts.port)
+ except ValueError:
+ server.run(host=opts.host)
+ else:
+ server.run(host=opts.host, port=port)
+ else:
+ print 'Invalid HTTP root directory: ' + opts.server
+ sys.exit(1)
+ else:
+ with utils.RunLock():
+ if unknown_opts:
+ LOG.warning('Unknown options: ' + ' '.join(unknown_opts))
+
+ # remove unfilled values
+ opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
+ result = nfvbench.run(opts)
+ if 'error_message' in result:
+ raise Exception(result['error_message'])
+
+ if 'result' in result and result['status']:
+ nfvbench.save(result['result'])
+ nfvbench.print_summary(result['result'])
+ except Exception:
+ LOG.error({
+ 'status': NFVBench.STATUS_ERROR,
+ 'error_message': traceback.format_exc()
+ })
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/nfvbench/nfvbenchd.py b/nfvbench/nfvbenchd.py
new file mode 100644
index 0000000..aef896a
--- /dev/null
+++ b/nfvbench/nfvbenchd.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python
+# Copyright 2017 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from flask import Flask
+from flask import jsonify
+from flask import render_template
+from flask import request
+
+from flask_socketio import emit
+from flask_socketio import SocketIO
+
+import json
+import Queue
+import traceback
+from utils import byteify
+from utils import RunLock
+import uuid
+
+# this global cannot reside in Ctx because of the @app and @socketio decorators
+app = None
+socketio = None
+
+STATUS_OK = 'OK'
+STATUS_ERROR = 'ERROR'
+STATUS_PENDING = 'PENDING'
+STATUS_NOT_FOUND = 'NOT_FOUND'
+
+
+def result_json(status, message, request_id=None):
+ body = {
+ 'status': status,
+ 'error_message': message
+ }
+
+ if request_id is not None:
+ body['request_id'] = request_id
+
+ return body
+
+
+def load_json(data):
+ return json.loads(json.dumps(data), object_hook=byteify)
+
+
+def get_uuid():
+ return uuid.uuid4().hex
+
+
+class Ctx(object):
+ MAXLEN = 5
+ run_queue = Queue.Queue()
+ busy = False
+ result = None
+ request_from_socketio = False
+ results = {}
+ ids = []
+ current_id = None
+
+ @staticmethod
+ def enqueue(config, request_id, from_socketio=False):
+ Ctx.busy = True
+ Ctx.request_from_socketio = from_socketio
+ config['request_id'] = request_id
+ Ctx.run_queue.put(config)
+
+ if len(Ctx.ids) >= Ctx.MAXLEN:
+ try:
+ del Ctx.results[Ctx.ids.pop(0)]
+ except KeyError:
+ pass
+ Ctx.ids.append(request_id)
+
+ @staticmethod
+ def dequeue():
+ config = Ctx.run_queue.get()
+ Ctx.current_id = config['request_id']
+ return config
+
+ @staticmethod
+ def release():
+ Ctx.current_id = None
+ Ctx.busy = False
+
+ @staticmethod
+ def set_result(res):
+ res['request_id'] = Ctx.current_id
+ Ctx.results[Ctx.current_id] = res
+ Ctx.result = res
+
+ @staticmethod
+ def get_result(request_id=None):
+ if request_id:
+ try:
+ res = Ctx.results[request_id]
+ except KeyError:
+ return None
+
+ if Ctx.result and request_id == Ctx.result['request_id']:
+ Ctx.result = None
+
+ return res
+ else:
+ res = Ctx.result
+ if res:
+ Ctx.result = None
+ return res
+
+ @staticmethod
+ def is_busy():
+ return Ctx.busy
+
+ @staticmethod
+ def get_current_request_id():
+ return Ctx.current_id
+
+
+def setup_flask(root_path):
+ global socketio
+ global app
+ app = Flask(__name__)
+ app.root_path = root_path
+ socketio = SocketIO(app, async_mode='threading')
+ busy_json = result_json(STATUS_ERROR, 'there is already an NFVbench request running')
+ not_busy_json = result_json(STATUS_ERROR, 'no pending NFVbench run')
+ not_found_msg = 'results not found'
+ pending_msg = 'NFVbench run still pending'
+
+ # --------- socketio requests ------------
+
+ @socketio.on('start_run')
+ def socketio_start_run(config):
+ if not Ctx.is_busy():
+ Ctx.enqueue(config, get_uuid(), from_socketio=True)
+ else:
+ emit('error', {'reason': 'there is already an NFVbench request running'})
+
+ @socketio.on('echo')
+ def socketio_echo(config):
+ emit('echo', config)
+
+ # --------- HTTP requests ------------
+
+ @app.route('/')
+ def index():
+ return render_template('index.html')
+
+ @app.route('/echo', methods=['GET'])
+ def echo():
+ config = request.json
+ return jsonify(config)
+
+ @app.route('/start_run', methods=['POST'])
+ def start_run():
+ config = load_json(request.json)
+ if Ctx.is_busy():
+ return jsonify(busy_json)
+ else:
+ request_id = get_uuid()
+ Ctx.enqueue(config, request_id)
+ return jsonify(result_json(STATUS_PENDING, pending_msg, request_id))
+
+ @app.route('/status', defaults={'request_id': None}, methods=['GET'])
+ @app.route('/status/<request_id>', methods=['GET'])
+ def get_status(request_id):
+ if request_id:
+ if Ctx.is_busy() and request_id == Ctx.get_current_request_id():
+ # task with request_id still pending
+ return jsonify(result_json(STATUS_PENDING, pending_msg, request_id))
+
+ res = Ctx.get_result(request_id)
+ if res:
+ # found result for given request_id
+ return jsonify(res)
+ else:
+ # result for given request_id not found
+ return jsonify(result_json(STATUS_NOT_FOUND, not_found_msg, request_id))
+ else:
+ if Ctx.is_busy():
+ # task still pending, return with request_id
+ return jsonify(result_json(STATUS_PENDING,
+ pending_msg,
+ Ctx.get_current_request_id()))
+
+ res = Ctx.get_result()
+ if res:
+ return jsonify(res)
+ else:
+ return jsonify(not_busy_json)
+
+
+class WebSocketIoServer(object):
+ """This class takes care of the web socketio server, accepts websocket events, and sends back
+ notifications using websocket events (send_ methods). Caller should simply create an instance
+ of this class and pass a runner object then invoke the run method
+ """
+ def __init__(self, http_root, runner):
+ self.nfvbench_runner = runner
+ setup_flask(http_root)
+
+ def run(self, host='127.0.0.1', port=7556):
+
+ # socketio.run will not return so we need to run it in a background thread so that
+ # the calling thread (main thread) can keep doing work
+ socketio.start_background_task(target=socketio.run, app=app, host=host, port=port)
+
+ # wait for run requests
+ # the runner must be executed from the main thread (Trex client library requirement)
+ while True:
+ # print 'main thread waiting for requests...'
+ config = Ctx.dequeue()
+ # print 'main thread processing request...'
+ print config
+ try:
+ # remove unfilled values as we do not want them to override default values with None
+ config = {k: v for k, v in config.items() if v is not None}
+ with RunLock():
+ results = self.nfvbench_runner.run(config)
+ except Exception as exc:
+ print 'NFVbench runner exception:'
+ traceback.print_exc()
+ results = result_json(STATUS_ERROR, str(exc))
+
+ if Ctx.request_from_socketio:
+ socketio.emit('run_end', results)
+ else:
+ # this might overwrite a previously unfetched result
+ Ctx.set_result(results)
+ Ctx.release()
+
+ def send_interval_stats(self, time_ms, tx_pps, rx_pps, drop_pct):
+ stats = {'time_ms': time_ms, 'tx_pps': tx_pps, 'rx_pps': rx_pps, 'drop_pct': drop_pct}
+ socketio.emit('run_interval_stats', stats)
+
+ def send_ndr_found(self, ndr_pps):
+ socketio.emit('ndr_found', {'rate_pps': ndr_pps})
+
+ def send_pdr_found(self, pdr_pps):
+ socketio.emit('pdr_found', {'rate_pps': pdr_pps})
diff --git a/nfvbench/nfvbenchvm/nfvbenchvm.conf b/nfvbench/nfvbenchvm/nfvbenchvm.conf
new file mode 100644
index 0000000..0b76244
--- /dev/null
+++ b/nfvbench/nfvbenchvm/nfvbenchvm.conf
@@ -0,0 +1,9 @@
+FORWARDER={forwarder}
+TG_MAC1={tg_mac1}
+TG_MAC2={tg_mac2}
+VNF_GATEWAY1_CIDR={vnf_gateway1_cidr}
+VNF_GATEWAY2_CIDR={vnf_gateway2_cidr}
+TG_NET1={tg_net1}
+TG_NET2={tg_net2}
+TG_GATEWAY1_IP={tg_gateway1_ip}
+TG_GATEWAY2_IP={tg_gateway2_ip}
diff --git a/nfvbench/packet_analyzer.py b/nfvbench/packet_analyzer.py
new file mode 100644
index 0000000..c01675b
--- /dev/null
+++ b/nfvbench/packet_analyzer.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from collections import OrderedDict
+from log import LOG
+
+
+class PacketAnalyzer(object):
+ """Analyze packet drop counter in a chain"""
+
+ def __init__(self):
+ self.last_packet_count = 0
+ self.chain = []
+
+ def record(self, interface, traffic_type):
+ """Records the counter of the next interface with the corresponding traffic type"""
+ if interface.is_no_op():
+ return
+ packet_count = interface.get_packet_count(traffic_type)
+ packet_drop_count = self.last_packet_count - packet_count
+ path_data = OrderedDict()
+ path_data['interface'] = interface.name
+ path_data['device'] = interface.device
+ path_data['packet_count'] = packet_count
+
+ if self.chain:
+ path_data['packet_drop_count'] = packet_drop_count
+
+ self.chain.append(path_data)
+ self.last_packet_count = packet_count
+
+ def get_analysis(self):
+ """Gets the analysis of packet drops"""
+ transmitted_packets = self.chain[0]['packet_count']
+
+ for (index, path_data) in enumerate(self.chain):
+ LOG.info('[Packet Analyze] Interface: %s' % (path_data['interface']))
+ LOG.info('[Packet Analyze] > Count: %d' % (path_data['packet_count']))
+
+ if index:
+ if transmitted_packets:
+ self.chain[index]['packet_drop_percentage'] = \
+ 100.0 * path_data['packet_drop_count'] / transmitted_packets
+ else:
+ self.chain[index]['packet_drop_percentage'] = float('nan')
+ LOG.info('[Packet Analyze] > Packet Drops: %d' %
+ (path_data['packet_drop_count']))
+ LOG.info('[Packet Analyze] > Percentage: %s' %
+ (path_data['packet_drop_percentage']))
+
+ return self.chain
diff --git a/nfvbench/service_chain.py b/nfvbench/service_chain.py
new file mode 100644
index 0000000..104cfb4
--- /dev/null
+++ b/nfvbench/service_chain.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from chain_managers import StageManager
+from collections import OrderedDict
+from log import LOG
+from specs import ChainType
+import time
+
+
+class ServiceChain(object):
+
+ def __init__(self, config, clients, cred, specs, factory, notifier=None):
+ self.config = config
+ self.clients = clients
+ self.cred = cred
+ self.specs = specs
+ self.factory = factory
+ self.notifier = notifier
+ self.chain_name = self.config.service_chain
+ self.vlans = None
+ self.stage_manager = None
+ self.stats_manager = None
+ LOG.info('ServiceChain initialized.')
+
+ def __set_helpers(self):
+ self.stage_manager = StageManager(self.config, self.cred, self.factory)
+ self.clients['vm'] = self.stage_manager
+ self.vlans = self.stage_manager.get_vlans()
+
+ STATS_CLASS = self.factory.get_stats_class(self.config.service_chain)
+ self.stats_manager = STATS_CLASS(self.config,
+ self.clients,
+ self.specs,
+ self.factory,
+ self.vlans,
+ self.notifier)
+
+ def __set_vlan_tags(self):
+ if self.config.vlan_tagging:
+ # override with user-specified vlans if configured
+ vlans = self.config.vlans if self.config.vlans else self.vlans[:2]
+ for vlan, device in zip(vlans, self.config.generator_config.devices):
+ self.stats_manager.set_vlan_tag(device, vlan)
+
+ def __get_result_per_frame_size(self, frame_size, bidirectional):
+ start_time = time.time()
+ traffic_result = {
+ frame_size: {}
+ }
+ result = {}
+ if not self.config.no_traffic:
+ self.clients['traffic'].set_traffic(frame_size, bidirectional)
+
+ if self.config.single_run:
+ result = self.stats_manager.run()
+ else:
+ results = self.clients['traffic'].get_ndr_and_pdr()
+
+ for dr in ['pdr', 'ndr']:
+ if dr in results:
+ traffic_result[frame_size][dr] = results[dr]
+ if 'warning' in results[dr]['stats'] and results[dr]['stats']['warning']:
+ traffic_result['warning'] = results[dr]['stats']['warning']
+ traffic_result[frame_size]['iteration_stats'] = results['iteration_stats']
+
+ result['analysis_duration_sec'] = time.time() - start_time
+ if self.config.single_run:
+ result['run_config'] = self.clients['traffic'].get_run_config(result)
+ required = result['run_config']['direction-total']['orig']['rate_pps']
+ actual = result['stats']['total_tx_rate']
+ warning = self.clients['traffic'].compare_tx_rates(required, actual)
+ if warning is not None:
+ result['run_config']['warning'] = warning
+
+ traffic_result[frame_size].update(result)
+ return traffic_result
+
+ def __get_chain_result(self):
+ result = OrderedDict()
+ for fs in self.config.frame_sizes:
+ result.update(self.__get_result_per_frame_size(fs, self.config.traffic.bidirectional))
+
+ chain_result = {
+ 'flow_count': self.config.flow_count,
+ 'service_chain_count': self.config.service_chain_count,
+ 'bidirectional': self.config.traffic.bidirectional,
+ 'profile': self.config.traffic.profile,
+ 'compute_nodes': self.stats_manager.get_compute_nodes_bios(),
+ 'result': result
+ }
+
+ return chain_result
+
+ def __setup_traffic(self):
+ self.clients['traffic'].setup()
+ if not self.config.no_traffic:
+ if self.config.service_chain == ChainType.EXT and not self.config.no_arp:
+ self.clients['traffic'].ensure_arp_successful()
+ self.clients['traffic'].ensure_end_to_end()
+
+ def run(self):
+ LOG.info('Starting {} chain...'.format(self.chain_name))
+ LOG.info('Dry run: {}'.format(self.config.no_traffic))
+ results = {}
+
+ self.__set_helpers()
+ self.__set_vlan_tags()
+ self.stage_manager.set_vm_macs()
+ self.__setup_traffic()
+ results[self.chain_name] = {'result': self.__get_chain_result()}
+
+ if self.config.service_chain == ChainType.PVVP:
+ results[self.chain_name]['mode'] = 'inter-node' \
+ if self.config.inter_node else 'intra-node'
+
+ LOG.info("Service chain '{}' run completed.".format(self.chain_name))
+ return results
+
+ def get_version(self):
+ return self.stats_manager.get_version()
+
+ def close(self):
+ self.stage_manager.close()
+ self.stats_manager.close()
diff --git a/nfvbench/specs.py b/nfvbench/specs.py
new file mode 100644
index 0000000..3f93df6
--- /dev/null
+++ b/nfvbench/specs.py
@@ -0,0 +1,93 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+
+class Encaps(object):
+ VLAN = "VLAN"
+ VxLAN = "VxLAN"
+ BASIC = "BASIC"
+
+ encaps_mapping = {
+ 'VLAN': VLAN,
+ 'VXLAN': VxLAN
+ }
+
+ @classmethod
+ def get(cls, network_type):
+ return cls.encaps_mapping.get(network_type.upper(), None)
+
+
+class ChainType(object):
+ PVP = "PVP"
+ PVVP = "PVVP"
+ EXT = "EXT"
+
+ chain_mapping = {
+ 'PVP': PVP,
+ 'PVVP': PVVP,
+ 'EXT': EXT
+ }
+
+ @classmethod
+ def get_chain_type(cls, chain):
+ return cls.chain_mapping.get(chain.upper(), None)
+
+
+class OpenStackSpec(object):
+
+ def __init__(self):
+ self.__vswitch = "BASIC"
+ self.__encaps = Encaps.BASIC
+
+ @property
+ def vswitch(self):
+ return self.__vswitch
+
+ @vswitch.setter
+ def vswitch(self, vsw):
+ if vsw is None:
+ raise Exception('Trying to set vSwitch as None.')
+
+ self.__vswitch = vsw.upper()
+
+ @property
+ def encaps(self):
+ return self.__encaps
+
+ @encaps.setter
+ def encaps(self, enc):
+ if enc is None:
+ raise Exception('Trying to set Encaps as None.')
+
+ self.__encaps = enc
+
+
+class RunSpec(object):
+
+ def __init__(self, no_vswitch_access, openstack_spec):
+ self.use_vswitch = (not no_vswitch_access) and openstack_spec.vswitch != "BASIC"
+
+
+class Specs(object):
+
+ def __init__(self):
+ self.openstack = None
+ self.run_spec = None
+
+ def set_openstack_spec(self, openstack_spec):
+ self.openstack = openstack_spec
+
+ def set_run_spec(self, run_spec):
+ self.run_spec = run_spec
diff --git a/nfvbench/stats_collector.py b/nfvbench/stats_collector.py
new file mode 100644
index 0000000..964d704
--- /dev/null
+++ b/nfvbench/stats_collector.py
@@ -0,0 +1,145 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import time
+
+
+class StatsCollector(object):
+ """Base class for all stats collector classes."""
+
+ def __init__(self, start_time):
+ self.start_time = start_time
+ self.stats = []
+
+ def get(self):
+ return self.stats
+
+ def peek(self):
+ return self.stats[-1]
+
+ @staticmethod
+ def _get_drop_percentage(drop_pkts, total_pkts):
+ return float(drop_pkts * 100) / total_pkts
+
+ @staticmethod
+ def _get_rx_pps(tx_pps, drop_percentage):
+ return (tx_pps * (100 - drop_percentage)) / 100
+
+ def _get_current_time_diff(self):
+ return int((time.time() - self.start_time) * 1000)
+
+
+class IntervalCollector(StatsCollector):
+ """Collects stats while traffic is running. Frequency is specified by 'interval_sec' setting."""
+
+ last_tx_pkts = 0
+ last_rx_pkts = 0
+ last_time = 0
+
+ def __init__(self, start_time):
+ StatsCollector.__init__(self, start_time)
+ self.notifier = None
+
+ def attach_notifier(self, notifier):
+ self.notifier = notifier
+
+ def add(self, stats):
+ if self.notifier:
+ current_stats = self.__compute_tx_rx_diff(stats)
+ self.notifier.send_interval_stats(**current_stats)
+
+ def reset(self):
+ # don't reset time!
+ self.last_rx_pkts = 0
+ self.last_tx_pkts = 0
+
+ def add_ndr_pdr(self, tag, stats):
+ if self.notifier:
+
+ current_time = self._get_current_time_diff()
+ rx_pps = self._get_rx_pps(stats['tx_pps'], stats['drop_percentage'])
+
+ self.last_tx_pkts = stats['tx_pps'] / 1000 * (current_time - self.last_time)
+ self.last_rx_pkts = rx_pps / 1000 * (current_time - self.last_time)
+ self.last_time = current_time
+
+ # 'drop_pct' key is an unfortunate name, since in iteration stats it means
+ # number of the packets. More suitable would be 'drop_percentage'.
+ # FDS frontend depends on this key
+ current_stats = {
+ '{}_pps'.format(tag): stats['tx_pps'],
+ 'tx_pps': stats['tx_pps'],
+ 'rx_pps': rx_pps,
+ 'drop_pct': stats['drop_percentage'],
+ 'time_ms': current_time
+ }
+
+ self.notifier.send_interval_stats(time_ms=current_stats['time_ms'],
+ tx_pps=current_stats['tx_pps'],
+ rx_pps=current_stats['rx_pps'],
+ drop_pct=current_stats['drop_pct'])
+ if tag == 'ndr':
+ self.notifier.send_ndr_found(stats['tx_pps'])
+ else:
+ self.notifier.send_pdr_found(stats['tx_pps'])
+
+ def __compute_tx_rx_diff(self, stats):
+ current_time = self._get_current_time_diff()
+ tx_diff = stats['overall']['tx']['total_pkts'] - self.last_tx_pkts
+ tx_pps = (tx_diff * 1000) / (current_time - self.last_time)
+ rx_diff = stats['overall']['rx']['total_pkts'] - self.last_rx_pkts
+ rx_pps = (rx_diff * 1000) / (current_time - self.last_time)
+
+ self.last_rx_pkts = stats['overall']['rx']['total_pkts']
+ self.last_tx_pkts = stats['overall']['tx']['total_pkts']
+ self.last_time = current_time
+
+ return {
+ 'tx_pps': tx_pps,
+ 'rx_pps': rx_pps,
+ 'drop_pct': max(0.0, (1 - (float(rx_pps) / tx_pps)) * 100),
+ 'time_ms': current_time
+ }
+
+
+class IterationCollector(StatsCollector):
+ """Collects stats after traffic is stopped. Frequency is specified by 'duration_sec' setting."""
+
+ def __init__(self, start_time):
+ StatsCollector.__init__(self, start_time)
+
+ def add(self, stats, tx_pps):
+ drop_percentage = self._get_drop_percentage(stats['overall']['rx']['dropped_pkts'],
+ stats['overall']['tx']['total_pkts'])
+
+ record = {
+ 'total_tx_pps': int(stats['total_tx_rate']),
+ 'tx_pps': tx_pps,
+ 'tx_pkts': stats['overall']['tx']['total_pkts'],
+ 'rx_pps': self._get_rx_pps(tx_pps, drop_percentage),
+ 'rx_pkts': stats['overall']['rx']['total_pkts'],
+ 'drop_pct': stats['overall']['rx']['dropped_pkts'],
+ 'drop_percentage': drop_percentage,
+ 'time_ms': int(time.time() * 1000)
+ }
+
+ if 'warning' in stats:
+ record['warning'] = stats['warning']
+
+ self.stats.append(record)
+
+ def add_ndr_pdr(self, tag, rate):
+ last_stats = self.peek()
+ last_stats['{}_pps'.format(tag)] = rate
diff --git a/nfvbench/summarizer.py b/nfvbench/summarizer.py
new file mode 100644
index 0000000..4ee2426
--- /dev/null
+++ b/nfvbench/summarizer.py
@@ -0,0 +1,402 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import bitmath
+from contextlib import contextmanager
+import math
+from specs import ChainType
+from tabulate import tabulate
+
+
+class Formatter(object):
+ """Collection of string formatter methods"""
+
+ @staticmethod
+ def fixed(data):
+ return data
+
+ @staticmethod
+ def int(data):
+ return '{:,}'.format(data)
+
+ @staticmethod
+ def float(decimal):
+ return lambda data: '%.{}f'.format(decimal) % (data)
+
+ @staticmethod
+ def standard(data):
+ if type(data) == int:
+ return Formatter.int(data)
+ elif type(data) == float:
+ return Formatter.float(4)(data)
+ else:
+ return Formatter.fixed(data)
+
+ @staticmethod
+ def suffix(suffix_str):
+ return lambda data: Formatter.standard(data) + suffix_str
+
+ @staticmethod
+ def bits(data):
+ # By default, `best_prefix` returns a value in byte format, this hack (multiply by 8.0)
+ # will convert it into bit format.
+ bit = 8.0 * bitmath.Bit(float(data))
+ bit = bit.best_prefix(bitmath.SI)
+ byte_to_bit_classes = {
+ 'kB': bitmath.kb,
+ 'MB': bitmath.Mb,
+ 'GB': bitmath.Gb,
+ 'TB': bitmath.Tb,
+ 'PB': bitmath.Pb,
+ 'EB': bitmath.Eb,
+ 'ZB': bitmath.Zb,
+ 'YB': bitmath.Yb,
+ }
+ bps = byte_to_bit_classes.get(bit.unit, bitmath.Bit).from_other(bit) / 8.0
+ if bps.unit != 'Bit':
+ return bps.format("{value:.4f} {unit}ps")
+ else:
+ return bps.format("{value:.4f} bps")
+
+ @staticmethod
+ def percentage(data):
+ if data is None:
+ return ''
+ elif math.isnan(data):
+ return '-'
+ else:
+ return Formatter.suffix('%')(Formatter.float(4)(data))
+
+
+class Table(object):
+ """ASCII readable table class"""
+
+ def __init__(self, header):
+ header_row, self.formatters = zip(*header)
+ self.data = [header_row]
+ self.columns = len(header_row)
+
+ def add_row(self, row):
+ assert(self.columns == len(row))
+ formatted_row = []
+ for entry, formatter in zip(row, self.formatters):
+ formatted_row.append(formatter(entry))
+ self.data.append(formatted_row)
+
+ def get_string(self, indent=0):
+ spaces = ' ' * indent
+ table = tabulate(self.data,
+ headers='firstrow',
+ tablefmt='grid',
+ stralign='center',
+ floatfmt='.2f')
+ return table.replace('\n', '\n' + spaces)
+
+
+class Summarizer(object):
+ """Generic summarizer class"""
+
+ indent_per_level = 2
+
+ def __init__(self):
+ self.indent_size = 0
+ self.marker_stack = [False]
+ self.str = ''
+
+ def __indent(self, marker):
+ self.indent_size += self.indent_per_level
+ self.marker_stack.append(marker)
+
+ def __unindent(self):
+ assert(self.indent_size >= self.indent_per_level)
+ self.indent_size -= self.indent_per_level
+ self.marker_stack.pop()
+
+ def __get_indent_string(self):
+ current_str = ' ' * self.indent_size
+ if self.marker_stack[-1]:
+ current_str = current_str[:-2] + '> '
+ return current_str
+
+ def _put(self, *args):
+ self.str += self.__get_indent_string()
+ if len(args) and type(args[-1]) == dict:
+ self.str += ' '.join(map(str, args[:-1])) + '\n'
+ self._put_dict(args[-1])
+ else:
+ self.str += ' '.join(map(str, args)) + '\n'
+
+ def _put_dict(self, data):
+ with self._create_block(False):
+ for key, value in data.iteritems():
+ if type(value) == dict:
+ self._put(key + ':')
+ self._put_dict(value)
+ else:
+ self._put(key + ':', value)
+
+ def _put_table(self, table):
+ self.str += self.__get_indent_string()
+ self.str += table.get_string(self.indent_size) + '\n'
+
+ def __str__(self):
+ return self.str
+
+ @contextmanager
+ def _create_block(self, marker=True):
+ self.__indent(marker)
+ yield
+ self.__unindent()
+
+
+class NFVBenchSummarizer(Summarizer):
+ """Summarize nfvbench json result"""
+
+ ndr_pdr_header = [
+ ('-', Formatter.fixed),
+ ('L2 Frame Size', Formatter.standard),
+ ('Rate (fwd+rev)', Formatter.bits),
+ ('Rate (fwd+rev)', Formatter.suffix(' pps')),
+ ('Avg Drop Rate', Formatter.suffix('%')),
+ ('Avg Latency (usec)', Formatter.standard),
+ ('Min Latency (usec)', Formatter.standard),
+ ('Max Latency (usec)', Formatter.standard)
+ ]
+
+ single_run_header = [
+ ('L2 Frame Size', Formatter.standard),
+ ('Drop Rate', Formatter.suffix('%')),
+ ('Avg Latency (usec)', Formatter.standard),
+ ('Min Latency (usec)', Formatter.standard),
+ ('Max Latency (usec)', Formatter.standard)
+ ]
+
+ config_header = [
+ ('Direction', Formatter.standard),
+ ('Requested TX Rate (bps)', Formatter.bits),
+ ('Actual TX Rate (bps)', Formatter.bits),
+ ('RX Rate (bps)', Formatter.bits),
+ ('Requested TX Rate (pps)', Formatter.suffix(' pps')),
+ ('Actual TX Rate (pps)', Formatter.suffix(' pps')),
+ ('RX Rate (pps)', Formatter.suffix(' pps'))
+ ]
+
+ chain_analysis_header = [
+ ('Interface', Formatter.standard),
+ ('Device', Formatter.standard),
+ ('Packets (fwd)', Formatter.standard),
+ ('Drops (fwd)', Formatter.standard),
+ ('Drop% (fwd)', Formatter.percentage),
+ ('Packets (rev)', Formatter.standard),
+ ('Drops (rev)', Formatter.standard),
+ ('Drop% (rev)', Formatter.percentage)
+ ]
+
+ direction_keys = ['direction-forward', 'direction-reverse', 'direction-total']
+ direction_names = ['Forward', 'Reverse', 'Total']
+
+ def __init__(self, result):
+ Summarizer.__init__(self)
+ self.result = result
+ self.config = self.result['config']
+ self.__summarize()
+
+ def __summarize(self):
+ self._put()
+ self._put('========== NFVBench Summary ==========')
+ self._put('Date:', self.result['date'])
+ self._put('NFVBench version', self.result['nfvbench_version'])
+ self._put('Openstack Neutron:', {
+ 'vSwitch': self.result['openstack_spec']['vswitch'],
+ 'Encapsulation': self.result['openstack_spec']['encaps']
+ })
+ self._put('Benchmarks:')
+ with self._create_block():
+ self._put('Networks:')
+ with self._create_block():
+ network_benchmark = self.result['benchmarks']['network']
+
+ self._put('Components:')
+ with self._create_block():
+ self._put('TOR:')
+ with self._create_block(False):
+ self._put('Type:', self.config['tor']['type'])
+ self._put('Traffic Generator:')
+ with self._create_block(False):
+ self._put('Profile:', self.config['generator_config']['name'])
+ self._put('Tool:', self.config['generator_config']['tool'])
+ if network_benchmark['versions']:
+ self._put('Versions:')
+ with self._create_block():
+ for component, version in network_benchmark['versions'].iteritems():
+ self._put(component + ':', version)
+
+ if self.config['ndr_run'] or self.config['pdr_run']:
+ self._put('Measurement Parameters:')
+ with self._create_block(False):
+ if self.config['ndr_run']:
+ self._put('NDR:', self.config['measurement']['NDR'])
+ if self.config['pdr_run']:
+ self._put('PDR:', self.config['measurement']['PDR'])
+
+ self._put('Service chain:')
+ for result in network_benchmark['service_chain'].iteritems():
+ with self._create_block():
+ self.__chain_summarize(*result)
+
+ def __chain_summarize(self, chain_name, chain_benchmark):
+ self._put(chain_name + ':')
+ if chain_name == ChainType.PVVP:
+ self._put('Mode:', chain_benchmark.get('mode'))
+ with self._create_block():
+ self._put('Traffic:')
+ with self._create_block(False):
+ self.__traffic_summarize(chain_benchmark['result'])
+
+ def __traffic_summarize(self, traffic_benchmark):
+ self._put('Profile:', traffic_benchmark['profile'])
+ self._put('Bidirectional:', traffic_benchmark['bidirectional'])
+ self._put('Flow count:', traffic_benchmark['flow_count'])
+ self._put('Service chains count:', traffic_benchmark['service_chain_count'])
+ self._put('Compute nodes:', traffic_benchmark['compute_nodes'].keys())
+ with self._create_block(False):
+ self._put()
+ if not self.config['no_traffic']:
+ self._put('Run Summary:')
+ self._put()
+ with self._create_block(False):
+ self._put_table(self.__get_summary_table(traffic_benchmark['result']))
+ try:
+ self._put()
+ self._put(traffic_benchmark['result']['warning'])
+ except KeyError:
+ pass
+
+ for entry in traffic_benchmark['result'].iteritems():
+ if 'warning' in entry:
+ continue
+ self.__chain_analysis_summarize(*entry)
+
+ def __chain_analysis_summarize(self, frame_size, analysis):
+ self._put()
+ self._put('L2 frame size:', frame_size)
+ if 'analysis_duration_sec' in analysis:
+ self._put('Chain analysis duration:',
+ Formatter.float(3)(analysis['analysis_duration_sec']), 'seconds')
+ if self.config['ndr_run']:
+ self._put('NDR search duration:', Formatter.float(0)(analysis['ndr']['time_taken_sec']),
+ 'seconds')
+ if self.config['pdr_run']:
+ self._put('PDR search duration:', Formatter.float(0)(analysis['pdr']['time_taken_sec']),
+ 'seconds')
+ self._put()
+
+ if not self.config['no_traffic'] and self.config['single_run']:
+ self._put('Run Config:')
+ self._put()
+ with self._create_block(False):
+ self._put_table(self.__get_config_table(analysis['run_config']))
+ if 'warning' in analysis['run_config'] and analysis['run_config']['warning']:
+ self._put()
+ self._put(analysis['run_config']['warning'])
+ self._put()
+
+ if 'packet_analysis' in analysis:
+ self._put('Chain Analysis:')
+ self._put()
+ with self._create_block(False):
+ self._put_table(self.__get_chain_analysis_table(analysis['packet_analysis']))
+ self._put()
+
+ def __get_summary_table(self, traffic_result):
+ if self.config['single_run']:
+ summary_table = Table(self.single_run_header)
+ else:
+ summary_table = Table(self.ndr_pdr_header)
+
+ if self.config['ndr_run']:
+ for frame_size, analysis in traffic_result.iteritems():
+ if frame_size == 'warning':
+ continue
+ summary_table.add_row([
+ 'NDR',
+ frame_size,
+ analysis['ndr']['rate_bps'],
+ int(analysis['ndr']['rate_pps']),
+ analysis['ndr']['stats']['overall']['drop_percentage'],
+ analysis['ndr']['stats']['overall']['avg_delay_usec'],
+ analysis['ndr']['stats']['overall']['min_delay_usec'],
+ analysis['ndr']['stats']['overall']['max_delay_usec']
+ ])
+ if self.config['pdr_run']:
+ for frame_size, analysis in traffic_result.iteritems():
+ if frame_size == 'warning':
+ continue
+ summary_table.add_row([
+ 'PDR',
+ frame_size,
+ analysis['pdr']['rate_bps'],
+ int(analysis['pdr']['rate_pps']),
+ analysis['pdr']['stats']['overall']['drop_percentage'],
+ analysis['pdr']['stats']['overall']['avg_delay_usec'],
+ analysis['pdr']['stats']['overall']['min_delay_usec'],
+ analysis['pdr']['stats']['overall']['max_delay_usec']
+ ])
+ if self.config['single_run']:
+ for frame_size, analysis in traffic_result.iteritems():
+ summary_table.add_row([
+ frame_size,
+ analysis['stats']['overall']['drop_rate_percent'],
+ analysis['stats']['overall']['rx']['avg_delay_usec'],
+ analysis['stats']['overall']['rx']['min_delay_usec'],
+ analysis['stats']['overall']['rx']['max_delay_usec']
+ ])
+ return summary_table
+
+ def __get_config_table(self, run_config):
+ config_table = Table(self.config_header)
+ for key, name in zip(self.direction_keys, self.direction_names):
+ if key not in run_config:
+ continue
+ config_table.add_row([
+ name,
+ run_config[key]['orig']['rate_bps'],
+ run_config[key]['tx']['rate_bps'],
+ run_config[key]['rx']['rate_bps'],
+ int(run_config[key]['orig']['rate_pps']),
+ int(run_config[key]['tx']['rate_pps']),
+ int(run_config[key]['rx']['rate_pps']),
+ ])
+ return config_table
+
+ def __get_chain_analysis_table(self, packet_analysis):
+ chain_analysis_table = Table(self.chain_analysis_header)
+ forward_analysis = packet_analysis['direction-forward']
+ reverse_analysis = packet_analysis['direction-reverse']
+ reverse_analysis.reverse()
+
+ for fwd, rev in zip(forward_analysis, reverse_analysis):
+ chain_analysis_table.add_row([
+ fwd['interface'],
+ fwd['device'],
+ fwd['packet_count'],
+ fwd.get('packet_drop_count', None),
+ fwd.get('packet_drop_percentage', None),
+ rev['packet_count'],
+ rev.get('packet_drop_count', None),
+ rev.get('packet_drop_percentage', None),
+ ])
+ return chain_analysis_table
diff --git a/nfvbench/tor_client.py b/nfvbench/tor_client.py
new file mode 100644
index 0000000..c8214c8
--- /dev/null
+++ b/nfvbench/tor_client.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+
+class TORClientException(Exception):
+ pass
+
+
+class BasicTORClient(object):
+
+ def __init__(self, config):
+ pass
+
+ def get_int_counters(self):
+ return {}
+
+ def get_vni_counters(self, vni):
+ return {}
+
+ def get_vni_interface(self, vni, counters):
+ return None
+
+ def get_vni_for_vlan(self, vlans):
+ return []
+
+ def attach_tg_interfaces(self, network_vlans, switch_ports):
+ pass
+
+ def clear_nve(self):
+ pass
+
+ def clear_interface(self, vni):
+ pass
+
+ def close(self):
+ pass
+
+ def get_version(self):
+ return {}
diff --git a/nfvbench/traffic_client.py b/nfvbench/traffic_client.py
new file mode 100644
index 0000000..8bfcd76
--- /dev/null
+++ b/nfvbench/traffic_client.py
@@ -0,0 +1,790 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from attrdict import AttrDict
+import bitmath
+from datetime import datetime
+from log import LOG
+from netaddr import IPNetwork
+from network import Interface
+import socket
+from specs import ChainType
+from stats_collector import IntervalCollector
+from stats_collector import IterationCollector
+import struct
+import time
+import traffic_gen.traffic_utils as utils
+
+
+class TrafficClientException(Exception):
+ pass
+
+
+class TrafficRunner(object):
+
+ def __init__(self, client, duration_sec, interval_sec=0):
+ self.client = client
+ self.start_time = None
+ self.duration_sec = duration_sec
+ self.interval_sec = interval_sec
+
+ def run(self):
+ LOG.info('Running traffic generator')
+ self.client.gen.clear_stats()
+ self.client.gen.start_traffic()
+ self.start_time = time.time()
+ return self.poll_stats()
+
+ def stop(self):
+ if self.is_running():
+ self.start_time = None
+ self.client.gen.stop_traffic()
+
+ def is_running(self):
+ return self.start_time is not None
+
+ def time_elapsed(self):
+ if self.is_running():
+ return time.time() - self.start_time
+ else:
+ return self.duration_sec
+
+ def poll_stats(self):
+ if not self.is_running():
+ return None
+ time_elapsed = self.time_elapsed()
+ if time_elapsed > self.duration_sec:
+ self.stop()
+ return None
+ time_left = self.duration_sec - time_elapsed
+ if self.interval_sec > 0.0:
+ if time_left <= self.interval_sec:
+ time.sleep(time_left)
+ self.stop()
+ else:
+ time.sleep(self.interval_sec)
+ else:
+ time.sleep(self.duration_sec)
+ self.stop()
+ return self.client.get_stats()
+
+
+class Device(object):
+
+ def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
+ gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
+ gateway_ip_addrs_step=None, chain_count=1, flow_count=1, vlan_tagging=False):
+ self.chain_count = chain_count
+ self.flow_count = flow_count
+ self.dst = None
+ self.port = port
+ self.switch_port = switch_port
+ self.vtep_vlan = vtep_vlan
+ self.vlan_tag = None
+ self.vlan_tagging = vlan_tagging
+ self.pci = pci
+ self.mac = None
+ self.vm_mac_list = None
+ subnet = IPNetwork(ip)
+ self.ip = subnet.ip.format()
+ self.ip_prefixlen = subnet.prefixlen
+ self.ip_addrs_step = ip_addrs_step
+ self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
+ self.gateway_ip_addrs_step = gateway_ip_addrs_step
+ self.ip_list = self.expand_ip(self.ip, self.ip_addrs_step, self.flow_count)
+ self.gateway_ip = gateway_ip
+ self.gateway_ip_list = self.expand_ip(self.gateway_ip,
+ self.gateway_ip_addrs_step,
+ self.chain_count)
+ self.tg_gateway_ip = tg_gateway_ip
+ self.tg_gateway_ip_list = self.expand_ip(self.tg_gateway_ip,
+ self.tg_gateway_ip_addrs_step,
+ self.chain_count)
+
+ def set_mac(self, mac):
+ if mac is None:
+ raise TrafficClientException('Trying to set traffic generator MAC address as None')
+ self.mac = mac
+
+ def set_destination(self, dst):
+ self.dst = dst
+
+ def set_vm_mac_list(self, vm_mac_list):
+ self.vm_mac_list = map(str, vm_mac_list)
+
+ def set_vlan_tag(self, vlan_tag):
+ if self.vlan_tagging and vlan_tag is None:
+ raise TrafficClientException('Trying to set VLAN tag as None')
+ self.vlan_tag = vlan_tag
+
+ def get_stream_configs(self, service_chain):
+ configs = []
+ flow_idx = 0
+ for chain_idx in xrange(self.chain_count):
+ current_flow_count = (self.flow_count - flow_idx) / (self.chain_count - chain_idx)
+ max_idx = flow_idx + current_flow_count - 1
+ ip_src_count = self.ip_to_int(self.ip_list[max_idx]) - \
+ self.ip_to_int(self.ip_list[flow_idx]) + 1
+ ip_dst_count = self.ip_to_int(self.dst.ip_list[max_idx]) - \
+ self.ip_to_int(self.dst.ip_list[flow_idx]) + 1
+
+ configs.append({
+ 'count': current_flow_count,
+ 'mac_src': self.mac,
+ 'mac_dst': self.dst.mac if service_chain == ChainType.EXT
+ else self.vm_mac_list[chain_idx],
+ 'ip_src_addr': self.ip_list[flow_idx],
+ 'ip_src_addr_max': self.ip_list[max_idx],
+ 'ip_src_count': ip_src_count,
+ 'ip_dst_addr': self.dst.ip_list[flow_idx],
+ 'ip_dst_addr_max': self.dst.ip_list[max_idx],
+ 'ip_dst_count': ip_dst_count,
+ 'ip_addrs_step': self.ip_addrs_step,
+ 'mac_discovery_gw': self.gateway_ip_list[chain_idx],
+ 'ip_src_tg_gw': self.tg_gateway_ip_list[chain_idx],
+ 'ip_dst_tg_gw': self.dst.tg_gateway_ip_list[chain_idx],
+ 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
+ })
+
+ flow_idx += current_flow_count
+ return configs
+
+ @classmethod
+ def expand_ip(cls, ip, step_ip, count):
+ if step_ip == 'random':
+ # Repeatable Random will used in the stream src/dst IP pairs, but we still need
+ # to expand the IP based on the number of chains and flows configured. So we use
+ # "0.0.0.1" as the step to have the exact IP flow ranges for every chain.
+ step_ip = '0.0.0.1'
+
+ step_ip_in_int = cls.ip_to_int(step_ip)
+ subnet = IPNetwork(ip)
+ ip_list = []
+ for _ in xrange(count):
+ ip_list.append(subnet.ip.format())
+ subnet = subnet.next(step_ip_in_int)
+ return ip_list
+
+ @staticmethod
+ def mac_to_int(mac):
+ return int(mac.translate(None, ":.- "), 16)
+
+ @staticmethod
+ def int_to_mac(i):
+ mac = format(i, 'x').zfill(12)
+ blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
+ return ':'.join(blocks)
+
+ @staticmethod
+ def ip_to_int(addr):
+ return struct.unpack("!I", socket.inet_aton(addr))[0]
+
+
+class RunningTrafficProfile(object):
+ """Represents traffic configuration for currently running traffic profile."""
+
+ DEFAULT_IP_STEP = '0.0.0.1'
+ DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
+
+ def __init__(self, config, generator_profile):
+ generator_config = self.__match_generator_profile(config.traffic_generator,
+ generator_profile)
+ self.generator_config = generator_config
+ self.service_chain = config.service_chain
+ self.service_chain_count = config.service_chain_count
+ self.flow_count = config.flow_count
+ self.host_name = generator_config.host_name
+ self.name = generator_config.name
+ self.tool = generator_config.tool
+ self.cores = generator_config.get('cores', 1)
+ self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
+ self.tg_gateway_ip_addrs_step = \
+ generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
+ self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
+ self.gateway_ips = generator_config.gateway_ip_addrs
+ self.ip = generator_config.ip
+ self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
+ self.vlan_tagging = config.vlan_tagging
+ self.no_arp = config.no_arp
+ self.src_device = None
+ self.dst_device = None
+ self.vm_mac_list = None
+ self.__prep_interfaces(generator_config)
+
+ def to_json(self):
+ return dict(self.generator_config)
+
+ def set_vm_mac_list(self, vm_mac_list):
+ self.src_device.set_vm_mac_list(vm_mac_list[0])
+ self.dst_device.set_vm_mac_list(vm_mac_list[1])
+
+ @staticmethod
+ def __match_generator_profile(traffic_generator, generator_profile):
+ generator_config = AttrDict(traffic_generator)
+ generator_config.pop('default_profile')
+ generator_config.pop('generator_profile')
+ matching_profile = filter(lambda profile: profile.name == generator_profile,
+ traffic_generator.generator_profile)
+ if len(matching_profile) != 1:
+ raise Exception('Traffic generator profile not found: ' + generator_profile)
+
+ generator_config.update(matching_profile[0])
+
+ return generator_config
+
+ def __prep_interfaces(self, generator_config):
+ src_config = {
+ 'chain_count': self.service_chain_count,
+ 'flow_count': self.flow_count / 2,
+ 'ip': generator_config.ip_addrs[0],
+ 'ip_addrs_step': self.ip_addrs_step,
+ 'gateway_ip': self.gateway_ips[0],
+ 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
+ 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
+ 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
+ 'vlan_tagging': self.vlan_tagging
+ }
+ dst_config = {
+ 'chain_count': self.service_chain_count,
+ 'flow_count': self.flow_count / 2,
+ 'ip': generator_config.ip_addrs[1],
+ 'ip_addrs_step': self.ip_addrs_step,
+ 'gateway_ip': self.gateway_ips[1],
+ 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
+ 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
+ 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
+ 'vlan_tagging': self.vlan_tagging
+ }
+
+ self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
+ self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
+ self.src_device.set_destination(self.dst_device)
+ self.dst_device.set_destination(self.src_device)
+
+ if self.service_chain == ChainType.EXT and not self.no_arp \
+ and not self.__are_unique(self.src_device.ip_list, self.dst_device.ip_list):
+ raise Exception('Computed IP addresses are not unique, choose different base. '
+ 'Start IPs: {start}. End IPs: {end}'
+ .format(start=self.src_device.ip_list,
+ end=self.dst_device.ip_list))
+
+ def __are_unique(self, list1, list2):
+ return set(list1).isdisjoint(set(list2))
+
+ @property
+ def devices(self):
+ return [self.src_device, self.dst_device]
+
+ @property
+ def vlans(self):
+ return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
+
+ @property
+ def ports(self):
+ return [self.src_device.port, self.dst_device.port]
+
+ @property
+ def switch_ports(self):
+ return [self.src_device.switch_port, self.dst_device.switch_port]
+
+ @property
+ def pcis(self):
+ return [self.src_device.pci, self.dst_device.pci]
+
+
+class TrafficGeneratorFactory(object):
+
+ def __init__(self, config):
+ self.config = config
+
+ def get_tool(self):
+ return self.config.generator_config.tool
+
+ def get_generator_client(self):
+ tool = self.get_tool().lower()
+ if tool == 'trex':
+ from traffic_gen import trex
+ return trex.TRex(self.config)
+ elif tool == 'dummy':
+ from traffic_gen import dummy
+ return dummy.DummyTG(self.config)
+ else:
+ return None
+
+ def list_generator_profile(self):
+ return [profile.name for profile in self.config.traffic_generator.generator_profile]
+
+ def get_generator_config(self, generator_profile):
+ return RunningTrafficProfile(self.config, generator_profile)
+
+ def get_matching_profile(self, traffic_profile_name):
+ matching_profile = filter(lambda profile: profile.name == traffic_profile_name,
+ self.config.traffic_profile)
+
+ if len(matching_profile) > 1:
+ raise Exception('Multiple traffic profiles with the same name found.')
+ elif len(matching_profile) == 0:
+ raise Exception('No traffic profile found.')
+
+ return matching_profile[0]
+
+ def get_frame_sizes(self, traffic_profile):
+ matching_profile = self.get_matching_profile(traffic_profile)
+ return matching_profile.l2frame_size
+
+
+class TrafficClient(object):
+
+ PORTS = [0, 1]
+
+ def __init__(self, config, notifier=None):
+ generator_factory = TrafficGeneratorFactory(config)
+ self.gen = generator_factory.get_generator_client()
+ self.tool = generator_factory.get_tool()
+ self.config = config
+ self.notifier = notifier
+ self.interval_collector = None
+ self.iteration_collector = None
+ self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
+ if self.gen is None:
+ raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
+
+ self.run_config = {
+ 'l2frame_size': None,
+ 'duration_sec': self.config.duration_sec,
+ 'bidirectional': True,
+ 'rates': None
+ }
+ self.current_total_rate = {'rate_percent': '10'}
+ if self.config.single_run:
+ self.current_total_rate = utils.parse_rate_str(self.config.rate)
+
+ def set_macs(self):
+ for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
+ device.set_mac(mac)
+
+ def start_traffic_generator(self):
+ self.gen.init()
+ self.gen.connect()
+
+ def setup(self):
+ self.gen.set_mode()
+ self.gen.config_interface()
+ self.gen.clear_stats()
+
+ def get_version(self):
+ return self.gen.get_version()
+
+ def ensure_end_to_end(self):
+ """
+ Ensure traffic generator receives packets it has transmitted.
+ This ensures end to end connectivity and also waits until VMs are ready to forward packets.
+
+ At this point all VMs are in active state, but forwarding does not have to work.
+ Small amount of traffic is sent to every chain. Then total of sent and received packets
+ is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
+ N being number of chains, traffic flows through every chain and real measurements can be
+ performed.
+
+ Example:
+ PVP chain (1 VM per chain)
+ N = 10 (number of chains)
+ threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
+ if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
+ all 10 VMs are in operational state.
+ """
+ LOG.info('Starting traffic generator to ensure end-to-end connectivity')
+ rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
+ self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
+
+ # ensures enough traffic is coming back
+ threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
+
+ for it in xrange(self.config.generic_retry_count):
+ self.gen.clear_stats()
+ self.gen.start_traffic()
+ LOG.info('Waiting for packets to be received back... ({} / {})'.format(it + 1,
+ self.config.generic_retry_count))
+ time.sleep(self.config.generic_poll_sec)
+ self.gen.stop_traffic()
+ stats = self.gen.get_stats()
+
+ # compute total sent and received traffic on both ports
+ total_rx = 0
+ total_tx = 0
+ for port in self.PORTS:
+ total_rx += float(stats[port]['rx'].get('total_pkts', 0))
+ total_tx += float(stats[port]['tx'].get('total_pkts', 0))
+
+ # how much of traffic came back
+ ratio = total_rx / total_tx if total_tx else 0
+
+ if ratio > threshold:
+ self.gen.clear_stats()
+ self.gen.clear_streamblock()
+ LOG.info('End-to-end connectivity ensured')
+ return
+
+ time.sleep(self.config.generic_poll_sec)
+
+ raise TrafficClientException('End-to-end connectivity cannot be ensured')
+
+ def ensure_arp_successful(self):
+ if not self.gen.resolve_arp():
+ raise TrafficClientException('ARP cannot be resolved')
+
+ def set_traffic(self, frame_size, bidirectional):
+ self.run_config['bidirectional'] = bidirectional
+ self.run_config['l2frame_size'] = frame_size
+ self.run_config['rates'] = [self.get_per_direction_rate()]
+ if bidirectional:
+ self.run_config['rates'].append(self.get_per_direction_rate())
+ else:
+ unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
+ if unidir_reverse_pps > 0:
+ self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
+
+ self.gen.clear_streamblock()
+ self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
+
+ def modify_load(self, load):
+ self.current_total_rate = {'rate_percent': str(load)}
+ rate_per_direction = self.get_per_direction_rate()
+
+ self.gen.modify_rate(rate_per_direction, False)
+ self.run_config['rates'][0] = rate_per_direction
+ if self.run_config['bidirectional']:
+ self.gen.modify_rate(rate_per_direction, True)
+ self.run_config['rates'][1] = rate_per_direction
+
+ def get_ndr_and_pdr(self):
+ dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
+ targets = {}
+ if self.config.ndr_run:
+ LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
+ targets['ndr'] = self.config.measurement.NDR
+ if self.config.pdr_run:
+ LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
+ targets['pdr'] = self.config.measurement.PDR
+
+ self.run_config['start_time'] = time.time()
+ self.interval_collector = IntervalCollector(self.run_config['start_time'])
+ self.interval_collector.attach_notifier(self.notifier)
+ self.iteration_collector = IterationCollector(self.run_config['start_time'])
+ results = {}
+ self.__range_search(0.0, 200.0, targets, results)
+
+ results['iteration_stats'] = {
+ 'ndr_pdr': self.iteration_collector.get()
+ }
+
+ if self.config.ndr_run:
+ LOG.info('NDR load: %s', results['ndr']['rate_percent'])
+ results['ndr']['time_taken_sec'] = \
+ results['ndr']['timestamp_sec'] - self.run_config['start_time']
+ if self.config.pdr_run:
+ LOG.info('PDR load: %s', results['pdr']['rate_percent'])
+ results['pdr']['time_taken_sec'] = \
+ results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
+ else:
+ LOG.info('PDR load: %s', results['pdr']['rate_percent'])
+ results['pdr']['time_taken_sec'] = \
+ results['pdr']['timestamp_sec'] - self.run_config['start_time']
+ return results
+
+ def __get_dropped_rate(self, result):
+ dropped_pkts = result['rx']['dropped_pkts']
+ total_pkts = result['tx']['total_pkts']
+ if not total_pkts:
+ return float('inf')
+ else:
+ return float(dropped_pkts) / total_pkts * 100
+
+ def get_stats(self):
+ stats = self.gen.get_stats()
+ retDict = {'total_tx_rate': stats['total_tx_rate']}
+ for port in self.PORTS:
+ retDict[port] = {'tx': {}, 'rx': {}}
+
+ tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
+ rx_keys = tx_keys + ['dropped_pkts']
+
+ for port in self.PORTS:
+ for key in tx_keys:
+ retDict[port]['tx'][key] = int(stats[port]['tx'][key])
+ for key in rx_keys:
+ try:
+ retDict[port]['rx'][key] = int(stats[port]['rx'][key])
+ except ValueError:
+ retDict[port]['rx'][key] = 0
+ retDict[port]['rx']['avg_delay_usec'] = float(stats[port]['rx']['avg_delay_usec'])
+ retDict[port]['rx']['min_delay_usec'] = float(stats[port]['rx']['min_delay_usec'])
+ retDict[port]['rx']['max_delay_usec'] = float(stats[port]['rx']['max_delay_usec'])
+ retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
+
+ ports = sorted(retDict.keys())
+ if self.run_config['bidirectional']:
+ retDict['overall'] = {'tx': {}, 'rx': {}}
+ for key in tx_keys:
+ retDict['overall']['tx'][key] = \
+ retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
+ for key in rx_keys:
+ retDict['overall']['rx'][key] = \
+ retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
+ total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
+ retDict[ports[1]]['rx']['total_pkts']]
+ avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
+ retDict[ports[1]]['rx']['avg_delay_usec']]
+ max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
+ retDict[ports[1]]['rx']['max_delay_usec']]
+ min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
+ retDict[ports[1]]['rx']['min_delay_usec']]
+ retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
+ retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
+ retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
+ for key in ['pkt_bit_rate', 'pkt_rate']:
+ for dirc in ['tx', 'rx']:
+ retDict['overall'][dirc][key] /= 2.0
+ else:
+ retDict['overall'] = retDict[ports[0]]
+ retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
+ return retDict
+
+ def __convert_rates(self, rate):
+ return utils.convert_rates(self.run_config['l2frame_size'],
+ rate,
+ self.config.generator_config.intf_speed)
+
+ def __ndr_pdr_found(self, tag, load):
+ rates = self.__convert_rates({'rate_percent': load})
+ self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
+ last_stats = self.iteration_collector.peek()
+ self.interval_collector.add_ndr_pdr(tag, last_stats)
+
+ def __format_output_stats(self, stats):
+ for key in (self.PORTS + ['overall']):
+ interface = stats[key]
+ stats[key] = {
+ 'tx_pkts': interface['tx']['total_pkts'],
+ 'rx_pkts': interface['rx']['total_pkts'],
+ 'drop_percentage': interface['drop_rate_percent'],
+ 'drop_pct': interface['rx']['dropped_pkts'],
+ 'avg_delay_usec': interface['rx']['avg_delay_usec'],
+ 'max_delay_usec': interface['rx']['max_delay_usec'],
+ 'min_delay_usec': interface['rx']['min_delay_usec'],
+ }
+
+ return stats
+
+ def __targets_found(self, rate, targets, results):
+ for tag, target in targets.iteritems():
+ LOG.info('Found {} ({}) load: {}'.format(tag, target, rate))
+ self.__ndr_pdr_found(tag, rate)
+ results[tag]['timestamp_sec'] = time.time()
+
+ def __range_search(self, left, right, targets, results):
+ '''Perform a binary search for a list of targets inside a [left..right] range or rate
+
+ left the left side of the range to search as a % the line rate (100 = 100% line rate)
+ indicating the rate to send on each interface
+ right the right side of the range to search as a % of line rate
+ indicating the rate to send on each interface
+ targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag" ('ndr', 'pdr')
+ results a dict to store results
+ '''
+ if len(targets) == 0:
+ return
+ LOG.info('Range search [{} .. {}] targets: {}'.format(left, right, targets))
+
+ # Terminate search when gap is less than load epsilon
+ if right - left < self.config.measurement.load_epsilon:
+ self.__targets_found(left, targets, results)
+ return
+
+ # Obtain the average drop rate in for middle load
+ middle = (left + right) / 2.0
+ stats, rates = self.__run_search_iteration(middle)
+
+ # Split target dicts based on the avg drop rate
+ left_targets = {}
+ right_targets = {}
+ for tag, target in targets.iteritems():
+ if stats['overall']['drop_rate_percent'] <= target:
+ # record the best possible rate found for this target
+ results[tag] = rates
+ results[tag].update({
+ 'load_percent_per_direction': middle,
+ 'stats': self.__format_output_stats(dict(stats)),
+ 'timestamp_sec': None
+ })
+ right_targets[tag] = target
+ else:
+ left_targets[tag] = target
+
+ # search lower half
+ self.__range_search(left, middle, left_targets, results)
+
+ # search upper half only if the upper rate does not exceed
+ # 100%, this only happens when the first search at 100%
+ # yields a DR that is < target DR
+ if middle >= 100:
+ self.__targets_found(100, right_targets, results)
+ else:
+ self.__range_search(middle, right, right_targets, results)
+
+ def __run_search_iteration(self, rate):
+ # set load
+ self.modify_load(rate)
+
+ # poll interval stats and collect them
+ for stats in self.run_traffic():
+ self.interval_collector.add(stats)
+ time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
+ if time_elapsed_ratio >= 1:
+ self.cancel_traffic()
+ self.interval_collector.reset()
+
+ # get stats from the run
+ stats = self.runner.client.get_stats()
+ current_traffic_config = self.get_traffic_config()
+ warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
+ stats['total_tx_rate'])
+ if warning is not None:
+ stats['warning'] = warning
+
+ # save reliable stats from whole iteration
+ self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
+ LOG.info('Average drop rate: {}'.format(stats['overall']['drop_rate_percent']))
+
+ return stats, current_traffic_config['direction-total']
+
+ @staticmethod
+ def log_stats(stats):
+ report = {
+ 'datetime': str(datetime.now()),
+ 'tx_packets': stats['overall']['tx']['total_pkts'],
+ 'rx_packets': stats['overall']['rx']['total_pkts'],
+ 'drop_packets': stats['overall']['rx']['dropped_pkts'],
+ 'drop_rate_percent': stats['overall']['drop_rate_percent']
+ }
+ LOG.info('TX: %(tx_packets)d; '
+ 'RX: %(rx_packets)d; '
+ 'Dropped: %(drop_packets)d; '
+ 'Drop rate: %(drop_rate_percent).4f%%',
+ report)
+
+ def run_traffic(self):
+ stats = self.runner.run()
+ while self.runner.is_running:
+ self.log_stats(stats)
+ yield stats
+ stats = self.runner.poll_stats()
+ if stats is None:
+ return
+ self.log_stats(stats)
+ LOG.info('Drop rate: {}'.format(stats['overall']['drop_rate_percent']))
+ yield stats
+
+ def cancel_traffic(self):
+ self.runner.stop()
+
+ def get_interface(self, port_index):
+ port = self.gen.port_handle[port_index]
+ tx, rx = 0, 0
+ if not self.config.no_traffic:
+ stats = self.get_stats()
+ if port in stats:
+ tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
+ return Interface('traffic-generator', self.tool.lower(), tx, rx)
+
+ def get_traffic_config(self):
+ config = {}
+ load_total = 0.0
+ bps_total = 0.0
+ pps_total = 0.0
+ for idx, rate in enumerate(self.run_config['rates']):
+ key = 'direction-forward' if idx == 0 else 'direction-reverse'
+ config[key] = {
+ 'l2frame_size': self.run_config['l2frame_size'],
+ 'duration_sec': self.run_config['duration_sec']
+ }
+ config[key].update(rate)
+ config[key].update(self.__convert_rates(rate))
+ load_total += float(config[key]['rate_percent'])
+ bps_total += float(config[key]['rate_bps'])
+ pps_total += float(config[key]['rate_pps'])
+ config['direction-total'] = dict(config['direction-forward'])
+ config['direction-total'].update({
+ 'rate_percent': load_total,
+ 'rate_pps': pps_total,
+ 'rate_bps': bps_total
+ })
+
+ return config
+
+ def get_run_config(self, results):
+ """Returns configuration which was used for the last run."""
+ r = {}
+ for idx, key in enumerate(["direction-forward", "direction-reverse"]):
+ tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
+ rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
+ r[key] = {
+ "orig": self.__convert_rates(self.run_config['rates'][idx]),
+ "tx": self.__convert_rates({'rate_pps': tx_rate}),
+ "rx": self.__convert_rates({'rate_pps': rx_rate})
+ }
+
+ total = {}
+ for direction in ['orig', 'tx', 'rx']:
+ total[direction] = {}
+ for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
+ total[direction][unit] = sum(map(lambda x: float(x[direction][unit]), r.values()))
+
+ r['direction-total'] = total
+ return r
+
+ @staticmethod
+ def compare_tx_rates(required, actual):
+ threshold = 0.9
+ are_different = False
+ try:
+ if float(actual) / required < threshold:
+ are_different = True
+ except ZeroDivisionError:
+ are_different = True
+
+ if are_different:
+ msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
+ "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
+ "to achieve the requested TX rate.".format(r=required, a=actual)
+ LOG.info(msg)
+ return msg
+
+ return None
+
+ def get_per_direction_rate(self):
+ divisor = 2 if self.run_config['bidirectional'] else 1
+ if 'rate_percent' in self.current_total_rate:
+ # don't split rate if it's percentage
+ divisor = 1
+
+ return utils.divide_rate(self.current_total_rate, divisor)
+
+ def close(self):
+ try:
+ self.gen.stop_traffic()
+ except Exception:
+ pass
+ self.gen.clear_stats()
+ self.gen.cleanup()
diff --git a/nfvbench/traffic_gen/__init__.py b/nfvbench/traffic_gen/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nfvbench/traffic_gen/__init__.py
diff --git a/nfvbench/traffic_gen/dummy.py b/nfvbench/traffic_gen/dummy.py
new file mode 100644
index 0000000..dabdc71
--- /dev/null
+++ b/nfvbench/traffic_gen/dummy.py
@@ -0,0 +1,95 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nfvbench.log import LOG
+
+from traffic_base import AbstractTrafficGenerator
+import traffic_utils as utils
+
+
+
+class DummyTG(AbstractTrafficGenerator):
+ """Experimental dummy traffic generator.
+
+ This traffic generator will pretend to generate traffic and return fake data.
+ Useful for unit testing without actually generating any traffic.
+ """
+
+ def __init__(self, runner):
+ AbstractTrafficGenerator.__init__(self, runner)
+ self.port_handle = []
+ self.rates = []
+
+ def get_version(self):
+ return "0.1"
+
+ def init(self):
+ pass
+
+ def connect(self):
+ ports = list(self.config.generator_config.ports)
+ self.port_handle = ports
+
+ def is_arp_successful(self):
+ return True
+
+ def config_interface(self):
+ pass
+
+ def create_traffic(self, l2frame_size, rates, bidirectional, latency=True):
+ pass
+
+ def modify_rate(self, rate, reverse):
+ port_index = int(reverse)
+ port = self.port_handle[port_index]
+ self.rates[port_index] = utils.to_rate_str(rate)
+ LOG.info('Modified traffic stream for %s, new rate=%s.' % (port, utils.to_rate_str(rate)))
+
+ def clear_streamblock(self):
+ pass
+
+ def get_stats(self):
+ result = {}
+ for ph in self.port_handle:
+ result[ph] = {
+ 'tx': {
+ 'total_pkts': 1000,
+ 'total_pkt_bytes': 100000,
+ 'pkt_rate': 100,
+ 'pkt_bit_rate': 1000000
+ },
+ 'rx': {
+ 'total_pkts': 1000,
+ 'total_pkt_bytes': 100000,
+ 'pkt_rate': 100,
+ 'pkt_bit_rate': 1000000,
+ 'dropped_pkts': 0
+ }
+ }
+ result[ph]['rx']['max_delay_usec'] = 10.0
+ result[ph]['rx']['min_delay_usec'] = 1.0
+ result[ph]['rx']['avg_delay_usec'] = 2.0
+ return result
+
+ def clear_stats(self):
+ pass
+
+ def start_traffic(self):
+ pass
+
+ def stop_traffic(self):
+ pass
+
+ def cleanup(self):
+ pass
diff --git a/nfvbench/traffic_gen/traffic_base.py b/nfvbench/traffic_gen/traffic_base.py
new file mode 100644
index 0000000..064b2a2
--- /dev/null
+++ b/nfvbench/traffic_gen/traffic_base.py
@@ -0,0 +1,89 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import abc
+
+class TrafficGeneratorException(Exception):
+ pass
+
+
+class AbstractTrafficGenerator(object):
+
+ # src_mac (6) + dst_mac (6) + mac_type (2) + frame_check (4) = 18
+ l2_header_size = 18
+
+ imix_l2_sizes = [64, 594, 1518]
+ imix_l3_sizes = [size - l2_header_size for size in imix_l2_sizes]
+ imix_ratios = [7, 4, 1]
+ imix_avg_l2_size = sum(map(
+ lambda imix: 1.0 * imix[0] * imix[1],
+ zip(imix_l2_sizes, imix_ratios))) / sum(imix_ratios)
+
+ def __init__(self, config):
+ self.config = config
+
+ @abc.abstractmethod
+ def get_version():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def init():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def connect():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def config_interface():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def create_traffic():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def modify_traffic():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def get_stats():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def clear_traffic():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def start_traffic():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def stop_traffic():
+ # Must be implemented by sub classes
+ return None
+
+ @abc.abstractmethod
+ def cleanup():
+ # Must be implemented by sub classes
+ return None
diff --git a/nfvbench/traffic_gen/traffic_utils.py b/nfvbench/traffic_gen/traffic_utils.py
new file mode 100644
index 0000000..e5dc463
--- /dev/null
+++ b/nfvbench/traffic_gen/traffic_utils.py
@@ -0,0 +1,160 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import bitmath
+from traffic_base import AbstractTrafficGenerator
+
+
+def convert_rates(l2frame_size, rate, intf_speed):
+ avg_packet_size = get_average_packet_size(l2frame_size)
+ if 'rate_pps' in rate:
+ initial_rate_type = 'rate_pps'
+ pps = rate['rate_pps']
+ bps = pps_to_bps(pps, avg_packet_size)
+ load = bps_to_load(bps, intf_speed)
+ elif 'rate_bps' in rate:
+ initial_rate_type = 'rate_bps'
+ bps = rate['rate_bps']
+ load = bps_to_load(bps, intf_speed)
+ pps = bps_to_pps(bps, avg_packet_size)
+ elif 'rate_percent' in rate:
+ initial_rate_type = 'rate_percent'
+ load = rate['rate_percent']
+ bps = load_to_bps(load, intf_speed)
+ pps = bps_to_pps(bps, avg_packet_size)
+ else:
+ raise Exception('Traffic config needs to have a rate type key')
+
+ return {
+ 'initial_rate_type': initial_rate_type,
+ 'rate_pps': pps,
+ 'rate_percent': load,
+ 'rate_bps': bps
+ }
+
+
+def get_average_packet_size(l2frame_size):
+ if l2frame_size.upper() == 'IMIX':
+ return AbstractTrafficGenerator.imix_avg_l2_size
+ else:
+ return float(l2frame_size)
+
+
+def load_to_bps(load_percentage, intf_speed):
+ return float(load_percentage) / 100.0 * intf_speed
+
+
+def bps_to_load(bps, intf_speed):
+ return float(bps) / intf_speed * 100.0
+
+
+def bps_to_pps(bps, avg_packet_size):
+ return float(bps) / (avg_packet_size + 20.0) / 8
+
+
+def pps_to_bps(pps, avg_packet_size):
+ return float(pps) * (avg_packet_size + 20.0) * 8
+
+
+def weighted_avg(weight, count):
+ if sum(weight):
+ return sum(map(lambda x: x[0] * x[1], zip(weight, count))) / sum(weight)
+ else:
+ return float('nan')
+
+multiplier_map = {
+ 'K': 1000,
+ 'M': 1000000,
+ 'G': 1000000000
+}
+
+def parse_rate_str(rate_str):
+ if rate_str.endswith('pps'):
+ rate_pps = rate_str[:-3]
+ if not rate_pps:
+ raise Exception('%s is missing a numeric value' % rate_str)
+ try:
+ multiplier = multiplier_map[rate_pps[-1].upper()]
+ rate_pps = rate_pps[:-1]
+ except KeyError:
+ multiplier = 1
+ rate_pps = int(rate_pps.strip()) * multiplier
+ if rate_pps <= 0:
+ raise Exception('%s is out of valid range' % rate_str)
+ return {'rate_pps': str(rate_pps)}
+ elif rate_str.endswith('ps'):
+ rate = rate_str.replace('ps', '').strip()
+ bit_rate = bitmath.parse_string(rate).bits
+ if bit_rate <= 0:
+ raise Exception('%s is out of valid range' % rate_str)
+ return {'rate_bps': str(int(bit_rate))}
+ elif rate_str.endswith('%'):
+ rate_percent = float(rate_str.replace('%', '').strip())
+ if rate_percent <= 0 or rate_percent > 100.0:
+ raise Exception('%s is out of valid range (must be 1-100%%)' % rate_str)
+ return {'rate_percent': str(rate_percent)}
+ else:
+ raise Exception('Unknown rate string format %s' % rate_str)
+
+
+def divide_rate(rate, divisor):
+ if 'rate_pps' in rate:
+ key = 'rate_pps'
+ value = int(rate[key])
+ elif 'rate_bps' in rate:
+ key = 'rate_bps'
+ value = int(rate[key])
+ else:
+ key = 'rate_percent'
+ value = float(rate[key])
+ value /= divisor
+ rate = dict(rate)
+ rate[key] = str(value) if value else str(1)
+ return rate
+
+
+def to_rate_str(rate):
+ if 'rate_pps' in rate:
+ pps = rate['rate_pps']
+ return '{}pps'.format(pps)
+ elif 'rate_bps' in rate:
+ bps = rate['rate_bps']
+ return '{}bps'.format(bps)
+ elif 'rate_percent' in rate:
+ load = rate['rate_percent']
+ return '{}%'.format(load)
+ else:
+ assert False
+
+
+def nan_replace(d):
+ """Replaces every occurence of 'N/A' with float nan."""
+ for k, v in d.iteritems():
+ if isinstance(v, dict):
+ nan_replace(v)
+ elif v == 'N/A':
+ d[k] = float('nan')
+
+
+def mac_to_int(mac):
+ """Converts MAC address to integer representation."""
+ return int(mac.translate(None, ":.- "), 16)
+
+
+def int_to_mac(i):
+ """Converts integer representation of MAC address to hex string."""
+ mac = format(i, 'x').zfill(12)
+ blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
+ return ':'.join(blocks)
diff --git a/nfvbench/traffic_gen/trex.py b/nfvbench/traffic_gen/trex.py
new file mode 100644
index 0000000..6c2a304
--- /dev/null
+++ b/nfvbench/traffic_gen/trex.py
@@ -0,0 +1,456 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from collections import defaultdict
+from itertools import count
+from nfvbench.log import LOG
+from nfvbench.specs import ChainType
+from nfvbench.traffic_server import TRexTrafficServer
+from nfvbench.utils import timeout
+from nfvbench.utils import TimeoutError
+import os
+import random
+import time
+import traceback
+from traffic_base import AbstractTrafficGenerator
+from traffic_base import TrafficGeneratorException
+import traffic_utils as utils
+
+
+from trex_stl_lib.api import CTRexVmInsFixHwCs
+from trex_stl_lib.api import Dot1Q
+from trex_stl_lib.api import Ether
+from trex_stl_lib.api import IP
+from trex_stl_lib.api import STLClient
+from trex_stl_lib.api import STLError
+from trex_stl_lib.api import STLFlowLatencyStats
+from trex_stl_lib.api import STLFlowStats
+from trex_stl_lib.api import STLPktBuilder
+from trex_stl_lib.api import STLScVmRaw
+from trex_stl_lib.api import STLStream
+from trex_stl_lib.api import STLTXCont
+from trex_stl_lib.api import STLVmFixChecksumHw
+from trex_stl_lib.api import STLVmFlowVar
+from trex_stl_lib.api import STLVmFlowVarRepetableRandom
+from trex_stl_lib.api import STLVmWrFlowVar
+from trex_stl_lib.api import UDP
+from trex_stl_lib.services.trex_stl_service_arp import STLServiceARP
+
+
+class TRex(AbstractTrafficGenerator):
+
+ LATENCY_PPS = 1000
+
+ def __init__(self, runner):
+ AbstractTrafficGenerator.__init__(self, runner)
+ self.client = None
+ self.id = count()
+ self.latencies = defaultdict(list)
+ self.stream_ids = defaultdict(list)
+ self.port_handle = []
+ self.streamblock = defaultdict(list)
+ self.rates = []
+ self.arps = {}
+
+ def get_version(self):
+ return self.client.get_server_version()
+
+ def extract_stats(self, in_stats):
+ utils.nan_replace(in_stats)
+ LOG.debug(in_stats)
+
+ result = {}
+ for ph in self.port_handle:
+ stats = self.__combine_stats(in_stats, ph)
+ result[ph] = {
+ 'tx': {
+ 'total_pkts': stats['tx_pkts']['total'],
+ 'total_pkt_bytes': stats['tx_bytes']['total'],
+ 'pkt_rate': stats['tx_pps']['total'],
+ 'pkt_bit_rate': stats['tx_bps']['total']
+ },
+ 'rx': {
+ 'total_pkts': stats['rx_pkts']['total'],
+ 'total_pkt_bytes': stats['rx_bytes']['total'],
+ 'pkt_rate': stats['rx_pps']['total'],
+ 'pkt_bit_rate': stats['rx_bps']['total'],
+ 'dropped_pkts': stats['tx_pkts']['total'] - stats['rx_pkts']['total']
+ }
+ }
+
+ lat = self.__combine_latencies(in_stats, ph)
+ result[ph]['rx']['max_delay_usec'] = lat.get('total_max', float('nan'))
+ result[ph]['rx']['min_delay_usec'] = lat.get('total_min', float('nan'))
+ result[ph]['rx']['avg_delay_usec'] = lat.get('average', float('nan'))
+
+ total_tx_pkts = result[0]['tx']['total_pkts'] + result[1]['tx']['total_pkts']
+ result["total_tx_rate"] = total_tx_pkts / self.config.duration_sec
+ return result
+
+ def __combine_stats(self, in_stats, port_handle):
+ """Traverses TRex result dictionary and combines stream stats. Used for combining latency
+ and regular streams together.
+ """
+ result = defaultdict(lambda: defaultdict(float))
+
+ for pg_id in [self.stream_ids[port_handle]] + self.latencies[port_handle]:
+ record = in_stats['flow_stats'][pg_id]
+ for stat_type, stat_type_values in record.iteritems():
+ for ph, value in stat_type_values.iteritems():
+ result[stat_type][ph] += value
+
+ return result
+
+ def __combine_latencies(self, in_stats, port_handle):
+ """Traverses TRex result dictionary and combines chosen latency stats."""
+ if not len(self.latencies[port_handle]):
+ return {}
+
+ result = defaultdict(float)
+ result['total_min'] = float("inf")
+ for lat_id in self.latencies[port_handle]:
+ lat = in_stats['latency'][lat_id]
+ result['dropped_pkts'] += lat['err_cntrs']['dropped']
+ result['total_max'] = max(lat['latency']['total_max'], result['total_max'])
+ result['total_min'] = min(lat['latency']['total_min'], result['total_min'])
+ result['average'] += lat['latency']['average']
+
+ result['average'] /= len(self.latencies[port_handle])
+
+ return result
+
+ def create_pkt(self, stream_cfg, l2frame_size):
+ # 46 = 14 (Ethernet II) + 4 (CRC Checksum) + 20 (IPv4) + 8 (UDP)
+ payload = 'x' * (max(64, int(l2frame_size)) - 46)
+
+ pkt_base = Ether(src=stream_cfg['mac_src'], dst=stream_cfg['mac_dst'])
+
+ if stream_cfg['vlan_tag'] is not None:
+ pkt_base /= Dot1Q(vlan=stream_cfg['vlan_tag'])
+
+ pkt_base /= IP() / UDP()
+
+ if stream_cfg['ip_addrs_step'] == 'random':
+ src_fv = STLVmFlowVarRepetableRandom(
+ name="ip_src",
+ min_value=stream_cfg['ip_src_addr'],
+ max_value=stream_cfg['ip_src_addr_max'],
+ size=4,
+ seed=random.randint(0, 32767),
+ limit=stream_cfg['ip_src_count'])
+ dst_fv = STLVmFlowVarRepetableRandom(
+ name="ip_dst",
+ min_value=stream_cfg['ip_dst_addr'],
+ max_value=stream_cfg['ip_dst_addr_max'],
+ size=4,
+ seed=random.randint(0, 32767),
+ limit=stream_cfg['ip_dst_count'])
+ else:
+ src_fv = STLVmFlowVar(
+ name="ip_src",
+ min_value=stream_cfg['ip_src_addr'],
+ max_value=stream_cfg['ip_src_addr'],
+ size=4,
+ op="inc",
+ step=stream_cfg['ip_addrs_step'])
+ dst_fv = STLVmFlowVar(
+ name="ip_dst",
+ min_value=stream_cfg['ip_dst_addr'],
+ max_value=stream_cfg['ip_dst_addr_max'],
+ size=4,
+ op="inc",
+ step=stream_cfg['ip_addrs_step'])
+
+ vm_param = [
+ src_fv,
+ STLVmWrFlowVar(fv_name="ip_src", pkt_offset="IP.src"),
+ dst_fv,
+ STLVmWrFlowVar(fv_name="ip_dst", pkt_offset="IP.dst"),
+ STLVmFixChecksumHw(l3_offset="IP",
+ l4_offset="UDP",
+ l4_type=CTRexVmInsFixHwCs.L4_TYPE_UDP)
+ ]
+
+ return STLPktBuilder(pkt=pkt_base / payload, vm=STLScVmRaw(vm_param))
+
+ def generate_streams(self, port_handle, stream_cfg, l2frame, isg=0.0, latency=True):
+ idx_lat = None
+ streams = []
+ if l2frame == 'IMIX':
+ for t, (ratio, l2_frame_size) in enumerate(zip(self.imix_ratios, self.imix_l2_sizes)):
+ pkt = self.create_pkt(stream_cfg, l2_frame_size)
+ streams.append(STLStream(packet=pkt,
+ isg=0.1 * t,
+ flow_stats=STLFlowStats(
+ pg_id=self.stream_ids[port_handle]),
+ mode=STLTXCont(pps=ratio)))
+
+ if latency:
+ idx_lat = self.id.next()
+ sl = STLStream(packet=pkt,
+ isg=isg,
+ flow_stats=STLFlowLatencyStats(pg_id=idx_lat),
+ mode=STLTXCont(pps=self.LATENCY_PPS))
+ streams.append(sl)
+ else:
+ pkt = self.create_pkt(stream_cfg, l2frame)
+ streams.append(STLStream(packet=pkt,
+ flow_stats=STLFlowStats(pg_id=self.stream_ids[port_handle]),
+ mode=STLTXCont()))
+
+ if latency:
+ idx_lat = self.id.next()
+ streams.append(STLStream(packet=pkt,
+ flow_stats=STLFlowLatencyStats(pg_id=idx_lat),
+ mode=STLTXCont(pps=self.LATENCY_PPS)))
+
+ if latency:
+ self.latencies[port_handle].append(idx_lat)
+
+ return streams
+
+ def init(self):
+ pass
+
+ @timeout(5)
+ def __connect(self, client):
+ client.connect()
+
+ def __connect_after_start(self):
+ # after start, Trex may take a bit of time to initialize
+ # so we need to retry a few times
+ for it in xrange(self.config.generic_retry_count):
+ try:
+ time.sleep(1)
+ self.client.connect()
+ break
+ except Exception as ex:
+ if it == (self.config.generic_retry_count - 1):
+ raise ex
+ LOG.info("Retrying connection to TRex (%s)...", ex.message)
+
+ def connect(self):
+ LOG.info("Connecting to TRex...")
+ server_ip = self.config.generator_config.ip
+
+ # Connect to TRex server
+ self.client = STLClient(server=server_ip)
+ try:
+ self.__connect(self.client)
+ except (TimeoutError, STLError) as e:
+ if server_ip == '127.0.0.1':
+ try:
+ self.__start_server()
+ self.__connect_after_start()
+ except (TimeoutError, STLError) as e:
+ LOG.error('Cannot connect to TRex')
+ LOG.error(traceback.format_exc())
+ logpath = '/tmp/trex.log'
+ if os.path.isfile(logpath):
+ # Wait for TRex to finish writing error message
+ last_size = 0
+ for it in xrange(self.config.generic_retry_count):
+ size = os.path.getsize(logpath)
+ if size == last_size:
+ # probably not writing anymore
+ break
+ last_size = size
+ time.sleep(1)
+ with open(logpath, 'r') as f:
+ message = f.read()
+ else:
+ message = e.message
+ raise TrafficGeneratorException(message)
+ else:
+ raise TrafficGeneratorException(e.message)
+
+ ports = list(self.config.generator_config.ports)
+ self.port_handle = ports
+ # Prepare the ports
+ self.client.reset(ports)
+
+ def set_mode(self):
+ if self.config.service_chain == ChainType.EXT and not self.config.no_arp:
+ self.__set_l3_mode()
+ else:
+ self.__set_l2_mode()
+
+ def __set_l3_mode(self):
+ self.client.set_service_mode(ports=self.port_handle, enabled=True)
+ for port, device in zip(self.port_handle, self.config.generator_config.devices):
+ try:
+ self.client.set_l3_mode(port=port,
+ src_ipv4=device.tg_gateway_ip,
+ dst_ipv4=device.dst.gateway_ip,
+ vlan=device.vlan_tag if device.vlan_tagging else None)
+ except STLError:
+ # TRex tries to resolve ARP already, doesn't have to be successful yet
+ continue
+ self.client.set_service_mode(ports=self.port_handle, enabled=False)
+
+ def __set_l2_mode(self):
+ self.client.set_service_mode(ports=self.port_handle, enabled=True)
+ for port, device in zip(self.port_handle, self.config.generator_config.devices):
+ for cfg in device.get_stream_configs(self.config.generator_config.service_chain):
+ self.client.set_l2_mode(port=port, dst_mac=cfg['mac_dst'])
+ self.client.set_service_mode(ports=self.port_handle, enabled=False)
+
+ def __start_server(self):
+ server = TRexTrafficServer()
+ server.run_server(self.config.generator_config)
+
+ def resolve_arp(self):
+ self.client.set_service_mode(ports=self.port_handle)
+ LOG.info('Polling ARP until successful')
+ resolved = 0
+ attempt = 0
+ for port, device in zip(self.port_handle, self.config.generator_config.devices):
+ ctx = self.client.create_service_ctx(port=port)
+
+ arps = [
+ STLServiceARP(ctx,
+ src_ip=cfg['ip_src_tg_gw'],
+ dst_ip=cfg['mac_discovery_gw'],
+ vlan=device.vlan_tag if device.vlan_tagging else None)
+ for cfg in device.get_stream_configs(self.config.generator_config.service_chain)
+ ]
+
+ for _ in xrange(self.config.generic_retry_count):
+ attempt += 1
+ try:
+ ctx.run(arps)
+ except STLError:
+ LOG.error(traceback.format_exc())
+ continue
+
+ self.arps[port] = [arp.get_record().dst_mac for arp in arps
+ if arp.get_record().dst_mac is not None]
+
+ if len(self.arps[port]) == self.config.service_chain_count:
+ resolved += 1
+ LOG.info('ARP resolved successfully for port {}'.format(port))
+ break
+ else:
+ failed = [arp.get_record().dst_ip for arp in arps
+ if arp.get_record().dst_mac is None]
+ LOG.info('Retrying ARP for: {} ({} / {})'.format(
+ failed, attempt, self.config.generic_retry_count))
+ time.sleep(self.config.generic_poll_sec)
+
+ self.client.set_service_mode(ports=self.port_handle, enabled=False)
+ return resolved == len(self.port_handle)
+
+ def config_interface(self):
+ pass
+
+ def __is_rate_enough(self, l2frame_size, rates, bidirectional, latency):
+ """Check if rate provided by user is above requirements. Applies only if latency is True."""
+ intf_speed = self.config.generator_config.intf_speed
+ if latency:
+ if bidirectional:
+ mult = 2
+ total_rate = 0
+ for rate in rates:
+ r = utils.convert_rates(l2frame_size, rate, intf_speed)
+ total_rate += int(r['rate_pps'])
+ else:
+ mult = 1
+ total_rate = utils.convert_rates(l2frame_size, rates[0], intf_speed)
+ # rate must be enough for latency stream and at least 1 pps for base stream per chain
+ required_rate = (self.LATENCY_PPS + 1) * self.config.service_chain_count * mult
+ result = utils.convert_rates(l2frame_size,
+ {'rate_pps': required_rate},
+ intf_speed * mult)
+ result['result'] = total_rate >= required_rate
+ return result
+
+ return {'result': True}
+
+ def create_traffic(self, l2frame_size, rates, bidirectional, latency=True):
+ r = self.__is_rate_enough(l2frame_size, rates, bidirectional, latency)
+ if not r['result']:
+ raise TrafficGeneratorException(
+ 'Required rate in total is at least one of: \n{pps}pps \n{bps}bps \n{load}%.'
+ .format(pps=r['rate_pps'],
+ bps=r['rate_bps'],
+ load=r['rate_percent']))
+
+ stream_cfgs = [d.get_stream_configs(self.config.generator_config.service_chain)
+ for d in self.config.generator_config.devices]
+ self.rates = map(lambda rate: utils.to_rate_str(rate), rates)
+
+ for ph in self.port_handle:
+ # generate one pg_id for each direction
+ self.stream_ids[ph] = self.id.next()
+
+ for i, (fwd_stream_cfg, rev_stream_cfg) in enumerate(zip(*stream_cfgs)):
+ if self.config.service_chain == ChainType.EXT and not self.config.no_arp:
+ fwd_stream_cfg['mac_dst'] = self.arps[self.port_handle[0]][i]
+ rev_stream_cfg['mac_dst'] = self.arps[self.port_handle[1]][i]
+
+ self.streamblock[0].extend(self.generate_streams(self.port_handle[0],
+ fwd_stream_cfg,
+ l2frame_size,
+ latency=latency))
+ if len(self.rates) > 1:
+ self.streamblock[1].extend(self.generate_streams(self.port_handle[1],
+ rev_stream_cfg,
+ l2frame_size,
+ isg=10.0,
+ latency=bidirectional and latency))
+
+ for ph in self.port_handle:
+ self.client.add_streams(self.streamblock[ph], ports=ph)
+ LOG.info('Created traffic stream for port %s.' % ph)
+
+ def modify_rate(self, rate, reverse):
+ port_index = int(reverse)
+ port = self.port_handle[port_index]
+ self.rates[port_index] = utils.to_rate_str(rate)
+ LOG.info('Modified traffic stream for %s, new rate=%s.' % (port, utils.to_rate_str(rate)))
+
+ def clear_streamblock(self):
+ self.streamblock = defaultdict(list)
+ self.latencies = defaultdict(list)
+ self.stream_ids = defaultdict(list)
+ self.rates = []
+ self.client.reset(self.port_handle)
+ LOG.info('Cleared all existing streams.')
+
+ def get_stats(self):
+ stats = self.client.get_pgid_stats()
+ return self.extract_stats(stats)
+
+ def get_macs(self):
+ return [self.client.get_port_attr(port=port)['src_mac'] for port in self.port_handle]
+
+ def clear_stats(self):
+ if self.port_handle:
+ self.client.clear_stats()
+
+ def start_traffic(self):
+ for port, rate in zip(self.port_handle, self.rates):
+ self.client.start(ports=port, mult=rate, duration=self.config.duration_sec, force=True)
+
+ def stop_traffic(self):
+ self.client.stop(ports=self.port_handle)
+
+ def cleanup(self):
+ if self.client:
+ try:
+ self.client.reset(self.port_handle)
+ self.client.disconnect()
+ except STLError:
+ # TRex does not like a reset while in disconnected state
+ pass
diff --git a/nfvbench/traffic_server.py b/nfvbench/traffic_server.py
new file mode 100644
index 0000000..05f20e5
--- /dev/null
+++ b/nfvbench/traffic_server.py
@@ -0,0 +1,64 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from log import LOG
+import os
+import subprocess
+import yaml
+
+class TrafficServerException(Exception):
+ pass
+
+class TrafficServer(object):
+ """Base class for traffic servers."""
+
+class TRexTrafficServer(TrafficServer):
+ """Creates configuration file for TRex and runs server."""
+
+ def __init__(self, trex_base_dir='/opt/trex'):
+ contents = os.listdir(trex_base_dir)
+ # only one version of TRex should be supported in container
+ assert(len(contents) == 1)
+ self.trex_dir = os.path.join(trex_base_dir, contents[0])
+
+ def run_server(self, traffic_profile, filename='/etc/trex_cfg.yaml'):
+ """
+ Runs TRex server for specified traffic profile.
+
+ :param traffic_profile: traffic profile object based on config file
+ :param filename: path where to save TRex config file
+ """
+ cfg = self.__save_config(traffic_profile, filename)
+ cores = traffic_profile.cores
+ subprocess.Popen(['nohup', '/bin/bash', '-c',
+ './t-rex-64 -i -c {} --iom 0 --no-scapy-server --close-at-end --vlan'
+ ' --cfg {} &> /tmp/trex.log & disown'.format(cores, cfg)],
+ cwd=self.trex_dir)
+ LOG.info('TRex server is running...')
+
+ def __save_config(self, traffic_profile, filename):
+ ifs = ",".join([repr(pci) for pci in traffic_profile.pcis])
+
+ result = """# Config generated by NFVBench tool
+ - port_limit : 2
+ version : 2
+ interfaces : [{ifs}]""".format(ifs=ifs)
+
+ yaml.safe_load(result)
+ if os.path.exists(filename):
+ os.remove(filename)
+ with open(filename, 'w') as f:
+ f.write(result)
+
+ return filename
diff --git a/nfvbench/utils.py b/nfvbench/utils.py
new file mode 100644
index 0000000..4d9749c
--- /dev/null
+++ b/nfvbench/utils.py
@@ -0,0 +1,170 @@
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import errno
+import fcntl
+from functools import wraps
+import json
+from log import LOG
+import os
+import re
+import signal
+import subprocess
+
+
+class TimeoutError(Exception):
+ pass
+
+
+def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
+ def decorator(func):
+ def _handle_timeout(signum, frame):
+ raise TimeoutError(error_message)
+
+ def wrapper(*args, **kwargs):
+ signal.signal(signal.SIGALRM, _handle_timeout)
+ signal.alarm(seconds)
+ try:
+ result = func(*args, **kwargs)
+ finally:
+ signal.alarm(0)
+ return result
+
+ return wraps(func)(wrapper)
+
+ return decorator
+
+
+def save_json_result(result, json_file, std_json_path, service_chain, service_chain_count,
+ flow_count, frame_sizes):
+ """Save results in json format file."""
+ filepaths = []
+ if json_file:
+ filepaths.append(json_file)
+ if std_json_path:
+ name_parts = [service_chain, str(service_chain_count), str(flow_count)] + list(frame_sizes)
+ filename = '-'.join(name_parts) + '.json'
+ filepaths.append(os.path.join(std_json_path, filename))
+
+ if filepaths:
+ for file_path in filepaths:
+ LOG.info('Saving results in json file: ' + file_path + "...")
+ with open(file_path, 'w') as jfp:
+ json.dump(result,
+ jfp,
+ indent=4,
+ sort_keys=True,
+ separators=(',', ': '),
+ default=lambda obj: obj.to_json())
+
+
+def byteify(data, ignore_dicts=False):
+ # if this is a unicode string, return its string representation
+ if isinstance(data, unicode):
+ return data.encode('utf-8')
+ # if this is a list of values, return list of byteified values
+ if isinstance(data, list):
+ return [byteify(item, ignore_dicts=ignore_dicts) for item in data]
+ # if this is a dictionary, return dictionary of byteified keys and values
+ # but only if we haven't already byteified it
+ if isinstance(data, dict) and not ignore_dicts:
+ return {byteify(key, ignore_dicts=ignore_dicts): byteify(value, ignore_dicts=ignore_dicts)
+ for key, value in data.iteritems()}
+ # if it's anything else, return it in its original form
+ return data
+
+
+def dict_to_json_dict(record):
+ return json.loads(json.dumps(record, default=lambda obj: obj.to_json()))
+
+
+def get_intel_pci(nic_ports):
+ """Returns the first two PCI addresses of sorted PCI list for Intel NIC (i40e, ixgbe)"""
+ hx = r'[0-9a-fA-F]'
+ regex = r'{hx}{{4}}:({hx}{{2}}:{hx}{{2}}\.{hx}{{1}}).*(drv={driver}|.*unused=.*{driver})'
+
+ try:
+ trex_base_dir = '/opt/trex'
+ contents = os.listdir(trex_base_dir)
+ trex_dir = os.path.join(trex_base_dir, contents[0])
+ process = subprocess.Popen(['python', 'dpdk_setup_ports.py', '-s'],
+ cwd=trex_dir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ devices, _ = process.communicate()
+ except Exception:
+ devices = ''
+
+ for driver in ['i40e', 'ixgbe']:
+ matches = re.findall(regex.format(hx=hx, driver=driver), devices)
+ if matches:
+ pcis = map(lambda x: x[0], matches)
+ if len(pcis) < 2:
+ continue
+ pcis.sort()
+ return [pcis[port_index] for port_index in nic_ports]
+
+ return []
+
+
+multiplier_map = {
+ 'K': 1000,
+ 'M': 1000000,
+ 'G': 1000000000
+}
+
+
+def parse_flow_count(flow_count):
+ flow_count = str(flow_count)
+ input_fc = flow_count
+ multiplier = 1
+ if flow_count[-1].upper() in multiplier_map:
+ multiplier = multiplier_map[flow_count[-1].upper()]
+ flow_count = flow_count[:-1]
+
+ try:
+ flow_count = int(flow_count)
+ except ValueError:
+ raise Exception("Unknown flow count format '{}'".format(input_fc))
+
+ return flow_count * multiplier
+
+
+class RunLock(object):
+ """
+ Attempts to lock file and run current instance of NFVbench as the first,
+ otherwise raises exception.
+ """
+
+ def __init__(self, path='/tmp/nfvbench.lock'):
+ self._path = path
+ self._fd = None
+
+ def __enter__(self):
+ try:
+ self._fd = os.open(self._path, os.O_CREAT)
+ fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except (OSError, IOError):
+ raise Exception('Other NFVbench process is running. Please wait')
+
+ def __exit__(self, *args):
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
+ os.close(self._fd)
+ self._fd = None
+
+ # Try to remove the lock file, but don't try too hard because it is unnecessary.
+ try:
+ os.unlink(self._path)
+ except (OSError, IOError):
+ pass
diff --git a/nfvbenchvm/README.rst b/nfvbenchvm/README.rst
new file mode 100644
index 0000000..baa6996
--- /dev/null
+++ b/nfvbenchvm/README.rst
@@ -0,0 +1,84 @@
+NFVBENCH VM IMAGE FOR OPENSTACK
++++++++++++++++++++++++++++++++
+
+This repo will build a centos 7 image with testpmd and VPP installed.
+The VM will come with a pre-canned user/password: nfvbench/nfvbench
+
+BUILD INSTRUCTIONS
+==================
+
+Pre-requisites
+--------------
+- must run on Linux
+- the following packages must be installed prior to using this script:
+ - git
+ - qemu-utils
+ - kpartx
+
+Build the image
+---------------
+- cd dib
+- update the version number for the image (if needed) by modifying __version__ in build-image.sh
+- setup your http_proxy if needed
+- bash build-image.sh
+
+IMAGE INSTANCE AND CONFIG
+=========================
+
+Interface Requirements
+----------------------
+The instance must be launched using OpenStack with 2 network interfaces.
+For best performance, it should use a flavor with:
+
+- 2 vCPU
+- 4 GB RAM
+- cpu pinning set to exclusive
+
+Auto-configuration
+------------------
+nfvbench VM will automatically find the two virtual interfaces to use, and use the forwarder specifed in the config file.
+
+In the case testpmd is used, testpmd will be launched with mac forwarding mode where the destination macs rewritten according to the config file.
+
+In the case VPP is used, VPP will set up a L3 router, and forwarding traffic from one port to the other.
+
+nfvbenchvm Config
+-----------------
+nfvbenchvm config file is located at ``/etc/nfvbenchvm.conf``.
+
+.. code-block:: bash
+
+ FORWARDER=VPP
+ TG_MAC0=00:10:94:00:0A:00
+ TG_MAC1=00:11:94:00:0A:00
+ VNF1_GATEWAY_CIDR=1.1.0.2/8
+ VNF2_GATEWAY_CIDR=2.2.0.2/8
+ TG1_NET=10.0.0.0/8
+ TG2_NET=20.0.0.0/8
+ TG1_GATEWAY_IP=1.1.0.100
+ TG1_GATEWAY_IP=2.2.0.100
+
+
+Launching nfvbenchvm VM
+-----------------------
+
+Normally this image will be used together with NFVBench, and the required configurations will be automatically generated and pushed to VM by NFVBench. If launched manually, no forwarder will be run. Users will have the full control to run either testpmd or VPP via VNC console.
+
+To check if testpmd is running, you can run this command in VNC console:
+
+.. code-block:: bash
+
+ sudo screen -r testpmd
+
+To check if VPP is running, you can run this command in VNC console:
+
+.. code-block:: bash
+
+ service vpp status
+
+
+Hardcoded Username and Password
+--------------------------------
+- Username: nfvbench
+- Password: nfvbench
+
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..e855161
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,8 @@
+-r requirements.txt
+docutils==0.12.0
+flake8>=2.3.0
+pylint>=1.3
+pep8>=1.5.7
+sphinx>=1.4.0
+sphinx_rtd_theme>=0.1.9
+tox>=1.9.0
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2bb548d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,26 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+pbr>=1.10.0,<2.0
+
+attrdict>=2.0.0
+bitmath>=1.3.1.1
+paramiko>=1.14.0
+prettytable>=0.7.2
+pytz>=2016.4
+six>=1.10.0
+python-glanceclient==2.6.0
+python-neutronclient<3,>=2.3.6
+python-novaclient>=2.18.1
+python-openstackclient>=0.4.1
+python-keystoneclient>=1.0.0
+pyyaml>=3.11
+pyzmq>=15.3.0
+requests>=2.13.0
+tabulate>=0.7.5
+flask>=0.12
+flask_socketio>=2.8.3
+backports.ssl-match-hostname==3.5.0.1 # via websocket-client
+socketIO-client==0.7.2
+websocket-client==0.40.0 # via socketio-client
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..e6ccb1a
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,46 @@
+[metadata]
+name = nfvbench
+summary = An NFV benchmarking tool for Mercury OpenStack
+description-file =
+ README.rst
+author = OpenStack
+author-email = openstack-dev@lists.openstack.org
+home-page = http://www.openstack.org/
+classifier =
+ Environment :: OpenStack
+ Intended Audience :: Developers
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: Apache Software License
+ Operating System :: POSIX :: Linux
+ Operating System :: MacOS
+ Programming Language :: Python
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 2.7
+
+[files]
+packages =
+ nfvbench
+
+[entry_points]
+console_scripts =
+ nfvbench = nfvbench.nfvbench:main
+ nfvbench_client = client.nfvbench_client:main
+ nfvbench_cleanup = client.nfvbench_cleanup:main
+
+[compile_catalog]
+directory = nfvbench/locale
+domain = nfvbench
+
+[update_catalog]
+domain = nfvbench
+output_dir = nfvbench/locale
+input_file = nfvbench/locale/nfvbench.pot
+
+[extract_messages]
+keywords = _ gettext ngettext l_ lazy_gettext
+mapping_file = babel.cfg
+output_file = nfvbench/locale/nfvbench.pot
+
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..36eead0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
+
+from setuptools.command.test import test
+from setuptools import setup
+import sys
+
+
+class Tox(test):
+ def initialize_options(self):
+ test.initialize_options(self)
+ self.tox_args = None
+
+ def finalize_options(self):
+ test.finalize_options(self)
+ self.test_args = []
+ self.test_suite = True
+
+ def run_tests(self):
+ import tox
+ sys.exit(tox.cmdline())
+
+
+if __name__ == '__main__':
+ setup(setup_requires=['pbr'], pbr=True,
+ tests_require=['tox'],
+ cmdclass={'test': Tox})
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..a59e519
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,17 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+hacking<0.11,>=0.10.0
+
+coverage>=3.6
+discover
+python-subunit>=0.0.18
+sphinx>=1.4.0
+sphinx_rtd_theme>=0.1.9
+oslosphinx>=2.5.0 # Apache-2.0
+oslotest>=1.10.0 # Apache-2.0
+testrepository>=0.0.18
+testscenarios>=0.4
+testtools>=1.4.0
+pytest>=3.0.2
diff --git a/test/test_nfvbench.py b/test/test_nfvbench.py
new file mode 100644
index 0000000..be80033
--- /dev/null
+++ b/test/test_nfvbench.py
@@ -0,0 +1,640 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from attrdict import AttrDict
+from nfvbench.connection import SSH
+from nfvbench.credentials import Credentials
+from nfvbench.network import Interface
+from nfvbench.network import Network
+from nfvbench.specs import Encaps
+import nfvbench.traffic_gen.traffic_utils as traffic_utils
+import os
+import pytest
+
+__location__ = os.path.realpath(os.path.join(os.getcwd(),
+ os.path.dirname(__file__)))
+
+
+@pytest.fixture
+def ssh(monkeypatch):
+ def mock_init(self, ssh_access, *args, **kwargs):
+ self.ssh_access = ssh_access
+ if ssh_access.private_key:
+ self.pkey = self._get_pkey(ssh_access.private_key)
+ else:
+ self.pkey = None
+ self._client = False
+ self.connect_timeout = 2
+ self.connect_retry_count = 1
+ self.connect_retry_wait_sec = 1
+ super(SSH, self).__init__()
+
+ monkeypatch.setattr(SSH, '__init__', mock_init)
+
+
+@pytest.fixture
+def openstack_vxlan_spec():
+ return AttrDict(
+ {
+ 'openstack': AttrDict({
+ 'vswitch': "VTS",
+ 'encaps': Encaps.VxLAN}
+ ),
+ 'run_spec': AttrDict({
+ 'use_vpp': True
+ })
+ }
+ )
+
+# =========================================================================
+# PVP Chain tests
+# =========================================================================
+
+def test_chain_interface():
+ iface = Interface('testname', 'vpp', tx_packets=1234, rx_packets=4321)
+ assert iface.name == 'testname'
+ assert iface.device == 'vpp'
+ assert iface.get_packet_count('tx') == 1234
+ assert iface.get_packet_count('rx') == 4321
+ assert iface.get_packet_count('wrong_key') == 0
+
+
+@pytest.fixture(scope='session')
+def iface1():
+ return Interface('iface1', 'trex', tx_packets=10000, rx_packets=1234)
+
+
+@pytest.fixture(scope='session')
+def iface2():
+ return Interface('iface2', 'n9k', tx_packets=1234, rx_packets=9901)
+
+
+@pytest.fixture(scope='session')
+def iface3():
+ return Interface('iface3', 'n9k', tx_packets=9900, rx_packets=1234)
+
+
+@pytest.fixture(scope='session')
+def iface4():
+ return Interface('iface4', 'vpp', tx_packets=1234, rx_packets=9801)
+
+
+@pytest.fixture(scope='session')
+def net1(iface1, iface2, iface3, iface4):
+ return Network([iface1, iface2, iface3, iface4], reverse=False)
+
+
+@pytest.fixture(scope='session')
+def net2(iface1, iface2, iface3):
+ return Network([iface1, iface2, iface3], reverse=True)
+
+
+def test_chain_network(net1, net2, iface1, iface2, iface3, iface4):
+ assert [iface1, iface2, iface3, iface4] == net1.get_interfaces()
+ assert [iface3, iface2, iface1] == net2.get_interfaces()
+ net2.add_interface(iface4)
+ assert [iface4, iface3, iface2, iface1] == net2.get_interfaces()
+
+
+"""
+def test_chain_analysis(net1, monkeypatch, openstack_vxlan_spec):
+ def mock_empty(self, *args, **kwargs):
+ pass
+
+ monkeypatch.setattr(ServiceChain, '_setup', mock_empty)
+
+ f = ServiceChain(AttrDict({'service_chain': 'DUMMY'}), [], {'tor': {}}, openstack_vxlan_spec,
+ lambda x, y, z: None)
+ result = f.get_analysis([net1])
+ assert result[1]['packet_drop_count'] == 99
+ assert result[1]['packet_drop_percentage'] == 0.99
+ assert result[2]['packet_drop_count'] == 1
+ assert result[2]['packet_drop_percentage'] == 0.01
+ assert result[3]['packet_drop_count'] == 99
+ assert result[3]['packet_drop_percentage'] == 0.99
+
+ net1.reverse = True
+ result = f.get_analysis([net1])
+ assert result[1]['packet_drop_count'] == 0
+ assert result[1]['packet_drop_percentage'] == 0.0
+ assert result[2]['packet_drop_count'] == 0
+ assert result[2]['packet_drop_percentage'] == 0.0
+ assert result[3]['packet_drop_count'] == 0
+ assert result[3]['packet_drop_percentage'] == 0.0
+
+
+@pytest.fixture
+def pvp_chain(monkeypatch, openstack_vxlan_spec):
+ tor_vni1 = Interface('vni-4097', 'n9k', 50, 77)
+ vsw_vni1 = Interface('vxlan_tunnel0', 'vpp', 77, 48)
+ vsw_vif1 = Interface('VirtualEthernet0/0/2', 'vpp', 48, 77)
+ vsw_vif2 = Interface('VirtualEthernet0/0/3', 'vpp', 77, 47)
+ vsw_vni2 = Interface('vxlan_tunnel1', 'vpp', 43, 77)
+ tor_vni2 = Interface('vni-4098', 'n9k', 77, 40)
+
+ def mock_init(self, *args, **kwargs):
+ self.vni_ports = [4097, 4098]
+ self.specs = openstack_vxlan_spec
+ self.clients = {
+ 'vpp': AttrDict({
+ 'set_interface_counters': lambda: None,
+ })
+ }
+ self.worker = AttrDict({
+ 'run': lambda: None,
+ })
+
+ def mock_empty(self, *args, **kwargs):
+ pass
+
+ def mock_get_network(self, traffic_port, vni_id, reverse=False):
+ if vni_id == 0:
+ return Network([tor_vni1, vsw_vni1, vsw_vif1], reverse)
+ else:
+ return Network([tor_vni2, vsw_vni2, vsw_vif2], reverse)
+
+ def mock_get_data(self):
+ return {}
+
+ monkeypatch.setattr(PVPChain, '_get_network', mock_get_network)
+ monkeypatch.setattr(PVPChain, '_get_data', mock_get_data)
+ monkeypatch.setattr(PVPChain, '_setup', mock_empty)
+ monkeypatch.setattr(VxLANWorker, '_clear_interfaces', mock_empty)
+ monkeypatch.setattr(PVPChain, '_generate_traffic', mock_empty)
+ monkeypatch.setattr(PVPChain, '__init__', mock_init)
+ return PVPChain(None, None, {'vm': None, 'vpp': None, 'tor': None, 'traffic': None}, None)
+
+
+def test_pvp_chain_run(pvp_chain):
+ result = pvp_chain.run()
+ expected_result = {
+ 'raw_data': {},
+ 'stats': None,
+ 'packet_analysis': {
+ 'direction-forward': [
+ OrderedDict([
+ ('interface', 'vni-4097'),
+ ('device', 'n9k'),
+ ('packet_count', 50)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel0'),
+ ('device', 'vpp'),
+ ('packet_count', 48),
+ ('packet_drop_count', 2),
+ ('packet_drop_percentage', 4.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/2'),
+ ('device', 'vpp'),
+ ('packet_count', 48),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/3'),
+ ('device', 'vpp'),
+ ('packet_count', 47),
+ ('packet_drop_count', 1),
+ ('packet_drop_percentage', 2.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel1'),
+ ('device', 'vpp'),
+ ('packet_count', 43),
+ ('packet_drop_count', 4),
+ ('packet_drop_percentage', 8.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vni-4098'),
+ ('device', 'n9k'),
+ ('packet_count', 40),
+ ('packet_drop_count', 3),
+ ('packet_drop_percentage', 6.0)
+ ])
+ ],
+ 'direction-reverse': [
+ OrderedDict([
+ ('interface', 'vni-4098'),
+ ('device', 'n9k'),
+ ('packet_count', 77)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel1'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/3'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/2'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel0'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vni-4097'),
+ ('device', 'n9k'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ])
+ ]
+ }
+ }
+ assert result == expected_result
+"""
+
+
+# =========================================================================
+# PVVP Chain tests
+# =========================================================================
+
+"""
+@pytest.fixture
+def pvvp_chain(monkeypatch, openstack_vxlan_spec):
+ tor_vni1 = Interface('vni-4097', 'n9k', 50, 77)
+ vsw_vni1 = Interface('vxlan_tunnel0', 'vpp', 77, 48)
+ vsw_vif1 = Interface('VirtualEthernet0/0/2', 'vpp', 48, 77)
+ vsw_vif3 = Interface('VirtualEthernet0/0/0', 'vpp', 77, 47)
+ vsw_vif4 = Interface('VirtualEthernet0/0/1', 'vpp', 45, 77)
+ vsw_vif2 = Interface('VirtualEthernet0/0/3', 'vpp', 77, 44)
+ vsw_vni2 = Interface('vxlan_tunnel1', 'vpp', 43, 77)
+ tor_vni2 = Interface('vni-4098', 'n9k', 77, 40)
+
+ def mock_init(self, *args, **kwargs):
+ self.vni_ports = [4099, 4100]
+ self.v2vnet = V2VNetwork()
+ self.specs = openstack_vxlan_spec
+ self.clients = {
+ 'vpp': AttrDict({
+ 'get_v2v_network': lambda reverse=None: Network([vsw_vif3, vsw_vif4], reverse),
+ 'set_interface_counters': lambda pvvp=None: None,
+ 'set_v2v_counters': lambda: None,
+ })
+ }
+ self.worker = AttrDict({
+ 'run': lambda: None,
+ })
+
+ def mock_empty(self, *args, **kwargs):
+ pass
+
+ def mock_get_network(self, traffic_port, vni_id, reverse=False):
+ if vni_id == 0:
+ return Network([tor_vni1, vsw_vni1, vsw_vif1], reverse)
+ else:
+ return Network([tor_vni2, vsw_vni2, vsw_vif2], reverse)
+
+ def mock_get_data(self):
+ return {}
+
+ monkeypatch.setattr(PVVPChain, '_get_network', mock_get_network)
+ monkeypatch.setattr(PVVPChain, '_get_data', mock_get_data)
+ monkeypatch.setattr(PVVPChain, '_setup', mock_empty)
+ monkeypatch.setattr(VxLANWorker, '_clear_interfaces', mock_empty)
+ monkeypatch.setattr(PVVPChain, '_generate_traffic', mock_empty)
+ monkeypatch.setattr(PVVPChain, '__init__', mock_init)
+
+ return PVVPChain(None, None, {'vm': None, 'vpp': None, 'tor': None, 'traffic': None}, None)
+
+
+def test_pvvp_chain_run(pvvp_chain):
+ result = pvvp_chain.run()
+
+ expected_result = {
+ 'raw_data': {},
+ 'stats': None,
+ 'packet_analysis':
+ {'direction-forward': [
+ OrderedDict([
+ ('interface', 'vni-4097'),
+ ('device', 'n9k'),
+ ('packet_count', 50)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel0'),
+ ('device', 'vpp'),
+ ('packet_count', 48),
+ ('packet_drop_count', 2),
+ ('packet_drop_percentage', 4.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/2'),
+ ('device', 'vpp'),
+ ('packet_count', 48),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/0'),
+ ('device', 'vpp'),
+ ('packet_count', 47),
+ ('packet_drop_count', 1),
+ ('packet_drop_percentage', 2.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/1'),
+ ('device', 'vpp'),
+ ('packet_count', 45),
+ ('packet_drop_count', 2),
+ ('packet_drop_percentage', 4.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/3'),
+ ('device', 'vpp'),
+ ('packet_count', 44),
+ ('packet_drop_count', 1),
+ ('packet_drop_percentage', 2.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel1'),
+ ('device', 'vpp'),
+ ('packet_count', 43),
+ ('packet_drop_count', 1),
+ ('packet_drop_percentage', 2.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vni-4098'),
+ ('device', 'n9k'),
+ ('packet_count', 40),
+ ('packet_drop_count', 3),
+ ('packet_drop_percentage', 6.0)
+ ])
+ ],
+ 'direction-reverse': [
+ OrderedDict([
+ ('interface', 'vni-4098'),
+ ('device', 'n9k'),
+ ('packet_count', 77)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel1'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/3'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/1'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/0'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'VirtualEthernet0/0/2'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vxlan_tunnel0'),
+ ('device', 'vpp'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ]),
+ OrderedDict([
+ ('interface', 'vni-4097'),
+ ('device', 'n9k'),
+ ('packet_count', 77),
+ ('packet_drop_count', 0),
+ ('packet_drop_percentage', 0.0)
+ ])
+ ]}
+ }
+ assert result == expected_result
+"""
+
+# =========================================================================
+# Traffic client tests
+# =========================================================================
+
+def test_parse_rate_str():
+ parse_rate_str = traffic_utils.parse_rate_str
+ try:
+ assert parse_rate_str('100%') == {'rate_percent': '100.0'}
+ assert parse_rate_str('37.5%') == {'rate_percent': '37.5'}
+ assert parse_rate_str('100%') == {'rate_percent': '100.0'}
+ assert parse_rate_str('60pps') == {'rate_pps': '60'}
+ assert parse_rate_str('60kpps') == {'rate_pps': '60000'}
+ assert parse_rate_str('6Mpps') == {'rate_pps': '6000000'}
+ assert parse_rate_str('6gpps') == {'rate_pps': '6000000000'}
+ assert parse_rate_str('80bps') == {'rate_bps': '80'}
+ assert parse_rate_str('80bps') == {'rate_bps': '80'}
+ assert parse_rate_str('80kbps') == {'rate_bps': '80000'}
+ assert parse_rate_str('80kBps') == {'rate_bps': '640000'}
+ assert parse_rate_str('80Mbps') == {'rate_bps': '80000000'}
+ assert parse_rate_str('80 MBps') == {'rate_bps': '640000000'}
+ assert parse_rate_str('80Gbps') == {'rate_bps': '80000000000'}
+ except Exception as exc:
+ assert False, exc.message
+
+ def should_raise_error(str):
+ try:
+ parse_rate_str(str)
+ except Exception:
+ return True
+ else:
+ assert False
+
+ assert should_raise_error('101')
+ assert should_raise_error('201%')
+ assert should_raise_error('10Kbps')
+ assert should_raise_error('0kbps')
+ assert should_raise_error('0pps')
+ assert should_raise_error('-1bps')
+
+def test_rate_conversion():
+ assert traffic_utils.load_to_bps(50, 10000000000) == pytest.approx(5000000000.0)
+ assert traffic_utils.load_to_bps(37, 10000000000) == pytest.approx(3700000000.0)
+ assert traffic_utils.load_to_bps(100, 10000000000) == pytest.approx(10000000000.0)
+
+ assert traffic_utils.bps_to_load(5000000000.0, 10000000000) == pytest.approx(50.0)
+ assert traffic_utils.bps_to_load(3700000000.0, 10000000000) == pytest.approx(37.0)
+ assert traffic_utils.bps_to_load(10000000000.0, 10000000000) == pytest.approx(100.0)
+
+ assert traffic_utils.bps_to_pps(500000, 64) == pytest.approx(744.047619048)
+ assert traffic_utils.bps_to_pps(388888, 1518) == pytest.approx(31.6066319896)
+ assert traffic_utils.bps_to_pps(9298322222, 340.3) == pytest.approx(3225895.85831)
+
+ assert traffic_utils.pps_to_bps(744.047619048, 64) == pytest.approx(500000)
+ assert traffic_utils.pps_to_bps(31.6066319896, 1518) == pytest.approx(388888)
+ assert traffic_utils.pps_to_bps(3225895.85831, 340.3) == pytest.approx(9298322222)
+
+
+"""
+@pytest.fixture
+def traffic_client(monkeypatch):
+
+ def mock_init(self, *args, **kwargs):
+ self.run_config = {
+ 'bidirectional': False,
+ 'l2frame_size': '64',
+ 'duration_sec': 30,
+ 'rates': [{'rate_percent': '10'}, {'rate_pps': '1'}]
+ }
+
+ self.config = AttrDict({
+ 'generator_config': {
+ 'intf_speed': 10000000000
+ },
+ 'ndr_run': True,
+ 'pdr_run': True,
+ 'single_run': False,
+ 'attempts': 1,
+ 'measurement': {
+ 'NDR': 0.0,
+ 'PDR': 0.1,
+ 'load_epsilon': 0.1
+ }
+ })
+
+ self.runner = AttrDict({
+ 'time_elapsed': lambda: 30,
+ 'stop': lambda: None,
+ 'client': AttrDict({'get_stats': lambda: None})
+ })
+
+ self.current_load = None
+ self.dummy_stats = {
+ 50.0: 72.6433562831,
+ 25.0: 45.6095059858,
+ 12.5: 0.0,
+ 18.75: 27.218642979,
+ 15.625: 12.68585861,
+ 14.0625: 2.47154392563,
+ 13.28125: 0.000663797066801,
+ 12.890625: 0.0,
+ 13.0859375: 0.0,
+ 13.18359375: 0.00359387347122,
+ 13.671875: 0.307939922531,
+ 13.4765625: 0.0207718516156,
+ 13.57421875: 0.0661795060969
+ }
+
+ def mock_modify_load(self, load):
+ self.run_config['rates'][0] = {'rate_percent': str(load)}
+ self.current_load = load
+
+ def mock_run_traffic(self):
+ yield {
+ 'overall': {
+ 'drop_rate_percent': self.dummy_stats[self.current_load],
+ 'rx': {
+ 'total_pkts': 1,
+ 'avg_delay_usec': 0.0,
+ 'max_delay_usec': 0.0,
+ 'min_delay_usec': 0.0
+ }
+ }
+ }
+
+ monkeypatch.setattr(TrafficClient, '__init__', mock_init)
+ monkeypatch.setattr(TrafficClient, 'modify_load', mock_modify_load)
+ monkeypatch.setattr(TrafficClient, 'run_traffic', mock_run_traffic)
+
+ return TrafficClient()
+
+
+def test_ndr_pdr_search(traffic_client):
+ expected_results = {
+ 'pdr': {
+ 'l2frame_size': '64',
+ 'initial_rate_type': 'rate_percent',
+ 'stats': {
+ 'overall': {
+ 'drop_rate_percent': 0.0661795060969,
+ 'min_delay_usec': 0.0,
+ 'avg_delay_usec': 0.0,
+ 'max_delay_usec': 0.0
+ }
+ },
+ 'load_percent_per_direction': 13.57421875,
+ 'rate_percent': 13.57422547,
+ 'rate_bps': 1357422547.0,
+ 'rate_pps': 2019974.0282738095,
+ 'duration_sec': 30
+ },
+ 'ndr': {
+ 'l2frame_size': '64',
+ 'initial_rate_type': 'rate_percent',
+ 'stats': {
+ 'overall': {
+ 'drop_rate_percent': 0.0,
+ 'min_delay_usec': 0.0,
+ 'avg_delay_usec': 0.0,
+ 'max_delay_usec': 0.0
+ }
+ },
+ 'load_percent_per_direction': 13.0859375,
+ 'rate_percent': 13.08594422,
+ 'rate_bps': 1308594422.0,
+ 'rate_pps': 1947313.1279761905,
+ 'duration_sec': 30
+ }
+ }
+
+ results = traffic_client.get_ndr_and_pdr()
+ assert len(results) == 2
+ for result in results.values():
+ result.pop('timestamp_sec')
+ result.pop('time_taken_sec')
+ assert results == expected_results
+"""
+
+# =========================================================================
+# Other tests
+# =========================================================================
+
+def test_no_credentials():
+ cred = Credentials('/completely/wrong/path/openrc', None, False)
+ if cred.rc_auth_url:
+ # shouldn't get valid data unless user set environment variables
+ assert False
+ else:
+ assert True
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..3f08b99
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,43 @@
+[tox]
+minversion = 1.6
+envlist = py27,pep8
+skipsdist = True
+
+[testenv]
+usedevelop = True
+install_command = pip install -U {opts} {packages}
+setenv =
+ VIRTUAL_ENV={envdir}
+deps = -r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
+changedir=test
+commands = py.test -q -s --basetemp={envtmpdir} {posargs}
+
+[testenv:pep8]
+commands = flake8 {toxinidir}
+
+[testenv:venv]
+commands = {posargs}
+
+[testenv:cover]
+commands = python setup.py testr --coverage --testr-args='{posargs}'
+
+[testenv:docs]
+commands = python setup.py build_sphinx
+
+[flake8]
+# H803 skipped on purpose per list discussion.
+# E123, E125 skipped as they are invalid PEP-8.
+max-line-length = 100
+show-source = True
+#E302: expected 2 blank linee
+#E303: too many blank lines (2)
+#H233: Python 3.x incompatible use of print operator
+#H236: Python 3.x incompatible __metaclass__, use six.add_metaclass()
+#H302: import only modules.
+#H404: multi line docstring should start without a leading new line
+#H405: multi line docstring summary not separated with an empty line
+#H904: Wrap long lines in parentheses instead of a backslash
+ignore = E123,E125,H803,E302,E303,H233,H236,H302,H404,H405,H904
+builtins = _
+exclude=venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build