aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--.gitreview5
-rw-r--r--README35
-rwxr-xr-xbootstrap.sh51
-rw-r--r--docs/tag_documentation.rst30
-rw-r--r--docs/topology_schemas2
-rwxr-xr-xmain.py233
-rw-r--r--pylint.cfg280
-rw-r--r--requirements.txt9
-rw-r--r--resources/__init__.py16
-rw-r--r--resources/libraries/__init__.py16
-rw-r--r--resources/libraries/bash/dut_setup.sh39
-rw-r--r--resources/libraries/python/DUTSetup.py41
-rw-r--r--resources/libraries/python/IPUtil.py43
-rw-r--r--resources/libraries/python/IPv4NodeAddress.py104
-rw-r--r--resources/libraries/python/IPv4Util.py499
-rw-r--r--resources/libraries/python/IPv6NodesAddr.py67
-rw-r--r--resources/libraries/python/IPv6Setup.py289
-rw-r--r--resources/libraries/python/IPv6Util.py101
-rw-r--r--resources/libraries/python/InterfaceSetup.py152
-rw-r--r--resources/libraries/python/PacketVerifier.py310
-rw-r--r--resources/libraries/python/SetupFramework.py137
-rw-r--r--resources/libraries/python/TGSetup.py32
-rw-r--r--resources/libraries/python/TrafficGenerator.py57
-rw-r--r--resources/libraries/python/TrafficScriptArg.py60
-rw-r--r--resources/libraries/python/TrafficScriptExecutor.py91
-rw-r--r--resources/libraries/python/VatConfigGenerator.py58
-rw-r--r--resources/libraries/python/VatExecutor.py197
-rw-r--r--resources/libraries/python/VppCounters.py105
-rw-r--r--resources/libraries/python/__init__.py16
-rw-r--r--resources/libraries/python/constants.py19
-rw-r--r--resources/libraries/python/parsers/JsonParser.py45
-rw-r--r--resources/libraries/python/parsers/__init__.py12
-rw-r--r--resources/libraries/python/ssh.py235
-rw-r--r--resources/libraries/python/topology.py539
-rw-r--r--resources/libraries/robot/bridge_domain.robot54
-rw-r--r--resources/libraries/robot/counters.robot39
-rw-r--r--resources/libraries/robot/default.robot26
-rw-r--r--resources/libraries/robot/interfaces.robot20
-rw-r--r--resources/libraries/robot/ipv4.robot51
-rw-r--r--resources/libraries/robot/ipv6.robot167
-rw-r--r--resources/libraries/robot/vat/interfaces.robot23
-rw-r--r--resources/templates/vat/add_ip_address.vat1
-rw-r--r--resources/templates/vat/add_route.vat1
-rw-r--r--resources/templates/vat/clear_interface.vat3
-rw-r--r--resources/templates/vat/del_ip_address.vat1
-rw-r--r--resources/templates/vat/del_route.vat1
-rw-r--r--resources/templates/vat/dump_interfaces.vat3
-rw-r--r--resources/templates/vat/flush_ip_addresses.vat1
-rw-r--r--resources/templates/vat/l2_bridge_domain.vat5
-rw-r--r--resources/templates/vat/l2_bridge_domain_gen.vat5
-rw-r--r--resources/templates/vat/l2xconnect.vat6
-rw-r--r--resources/templates/vat/set_if_state.vat1
-rw-r--r--resources/topology_schemas/3_node_topology.sch.yaml49
-rw-r--r--resources/topology_schemas/topology.sch.yaml113
-rwxr-xr-xresources/traffic_scripts/icmpv6_echo.py99
-rwxr-xr-xresources/traffic_scripts/icmpv6_echo_req_resp.py185
-rwxr-xr-xresources/traffic_scripts/ipv4_ping_ttl_check.py124
-rwxr-xr-xresources/traffic_scripts/ipv6_ns.py104
-rwxr-xr-xresources/traffic_scripts/ipv6_sweep_ping.py110
-rwxr-xr-xresources/traffic_scripts/send_ip_icmp.py71
-rw-r--r--tests/suites/__init__.robot20
-rw-r--r--tests/suites/bridge_domain/test.robot39
-rw-r--r--tests/suites/ipv4/ipv4_untagged.robot58
-rw-r--r--tests/suites/ipv6/ipv6_untagged.robot52
-rw-r--r--tests/suites/performance/short.robot41
-rw-r--r--topologies/available/3_node_hw_topo1.yaml.example69
-rw-r--r--topologies/available/README1
-rw-r--r--topologies/enabled/README2
69 files changed, 5478 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..304828a8ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/env
+outputs
+output.xml
+log.html
+report.html
+*.pyc
+*~
+*.log
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000000..edd22cc030
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.fd.io
+port=29418
+project=csit.git
+
diff --git a/README b/README
new file mode 100644
index 0000000000..f3d9397aea
--- /dev/null
+++ b/README
@@ -0,0 +1,35 @@
+# STEPS TO START DEVELOPING TESTS LOCALLY
+ - install virtualenv
+ - generate environment using virtualenv:
+ # cd $ROOT
+ # virtualenv env
+ # source env/bin/activate
+ - install python requirements for this project by executing:
+ # pip install -r requirements.txt
+ - make sure user mentioned in topology.py has NOPASSWD sudo access to
+ vpe_api_test
+
+
+ Done.
+
+# STEPS TO START THE TESTS
+export PYTHONPATH=.
+
+# create topology, edit ip addresses
+cp topologies/available/topology.yaml.example topologies/available/topology.yaml
+ln -s ../available/topology.yaml topologies/enabled/topology.yaml
+
+pybot -L TRACE -v TOPOLOGY_PATH:topologies/enabled/topology.yaml tests
+ or
+./main.py -t topologies/enabled/topology.yaml -i test_tag
+ or
+./main.py
+
+
+# Dependencies on Nodes
+
+ - virtualenv
+ - pip
+ - python2.7
+ - python-dev package
+
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100755
index 0000000000..835759c6ac
--- /dev/null
+++ b/bootstrap.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -euf -o pipefail
+
+#git clone ssh://rotterdam-jobbuilder@gerrit.fd.io:29418/vpp
+#
+#cd vpp/build-root
+#./bootstrap.sh
+#make PLATFORM=vpp TAG=vpp_debug install-deb
+#
+#ls -la
+
+set -x
+
+ping 10.30.51.17 -w 3 || true
+ping 10.30.51.18 -w 3 || true
+ping 10.30.51.16 -w 3 || true
+ping 10.30.51.21 -w 3 || true
+ping 10.30.51.22 -w 3 || true
+ping 10.30.51.20 -w 3 || true
+ping 10.30.51.25 -w 3 || true
+ping 10.30.51.26 -w 3 || true
+ping 10.30.51.24 -w 3 || true
+
+
+#IFS=',' read -ra ADDR <<< "${JCLOUDS_IPS}"
+#
+#function ssh_do() {
+# echo
+# echo "### " ssh $@
+# ssh $@
+#}
+#
+#
+#set
+#
+#for addr in "${ADDR[@]}"; do
+# echo
+# echo ${addr}
+# echo
+#
+# ssh_do localadmin@${addr} hostname || true
+# ssh_do localadmin@${addr} ifconfig -a || true
+# ssh_do localadmin@${addr} lspci -Dnn || true
+# ssh_do localadmin@${addr} "lspci -Dnn | grep 0200" || true
+# ssh_do localadmin@${addr} free -m || true
+# ssh_do localadmin@${addr} cat /proc/meminfo || true
+#done
+
+
+
+
diff --git a/docs/tag_documentation.rst b/docs/tag_documentation.rst
new file mode 100644
index 0000000000..188f3b1d29
--- /dev/null
+++ b/docs/tag_documentation.rst
@@ -0,0 +1,30 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+Documentation for tags used to select and identify test cases.
+
+List of TAGs and their descriptions
+===================================
+
+Topology TAGs
+-------------
+
+3_NODE_DOUBLE_LINK_TOPO
+ 3 nodes connected in a circular topology with two links interconnecting
+ the devices.
+
+Objective TAGs
+--------------
+
+Environment TAGs
+----------------
diff --git a/docs/topology_schemas b/docs/topology_schemas
new file mode 100644
index 0000000000..d25e99ad76
--- /dev/null
+++ b/docs/topology_schemas
@@ -0,0 +1,2 @@
+http://www.kuwata-lab.com/kwalify/ruby/users-guide.html
+http://www.kuwata-lab.com/kwalify/ruby/users-guide.02.html#tips-merge
diff --git a/main.py b/main.py
new file mode 100755
index 0000000000..d575567854
--- /dev/null
+++ b/main.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 to make test execution easy."""
+
+from __future__ import print_function
+import sys
+import os
+import time
+from string import ascii_lowercase
+from random import sample
+import argparse
+from pykwalify.core import Core
+from pykwalify.errors import PyKwalifyException
+from yaml import load
+import robot
+from robot.errors import DATA_ERROR, DataError, FRAMEWORK_ERROR, FrameworkError
+from robot.run import RobotFramework
+from robot.conf.settings import RobotSettings
+from robot.running.builder import TestSuiteBuilder
+from robot.running.model import TestSuite
+
+TOPOLOGIES_DIR = './topologies/enabled/'
+TESTS_DIR = './tests'
+OUTPUTS_DIR = './outputs'
+
+
+def get_suite_list(*datasources, **options):
+ """Returns filtered test suites based on include exclude tags
+
+ :param datasources: paths to tests
+ :param options: Robot Framework options (robot.conf.settings.py)
+ :return: list of Robot Framework TestSuites which contain tests
+ """
+ class _MyRobotFramework(RobotFramework):
+ """Custom implementation of RobotFramework main()."""
+ def main(self, datasources, **options):
+ # copied from robot.run.RobotFramework.main
+ settings = RobotSettings(options)
+ test_suite = TestSuiteBuilder(settings['SuiteNames'],
+ settings['WarnOnSkipped'])
+ # pylint: disable=star-args
+ suite = test_suite.build(*datasources)
+ suite.configure(**settings.suite_config)
+
+ return suite
+
+ # get all test cases list without run tests, execute runs overloaded main
+ # function
+ suite = _MyRobotFramework().execute(*datasources, output=None, dryrun=True,
+ **options)
+ if isinstance(suite, TestSuite):
+ suites = []
+ suites.append(suite)
+ append_new = True
+ while append_new:
+ append_new = False
+ tmp = []
+ for suite in suites:
+ # pylint: disable=protected-access
+ if len(suite.suites._items) > 0:
+ for i in suite.suites._items:
+ tmp.append(i)
+ append_new = True
+ else:
+ tmp.append(suite)
+ suites = tmp
+ return suites
+ # TODO: check testcases Tags ? all tests should have same set of tags
+ else:
+ if suite == DATA_ERROR:
+ raise DataError
+ if suite == FRAMEWORK_ERROR:
+ raise FrameworkError
+ return []
+
+
+def run_suites(tests_dir, suites, output_dir, output_prefix='suite',
+ **options):
+ """Execute RF's run with parameters."""
+
+ with open('{}/{}.out'.format(output_dir, output_prefix), 'w') as out:
+ robot.run(tests_dir,
+ suite=[s.longname for s in suites],
+ output='{}/{}.xml'.format(output_dir, output_prefix),
+ debugfile='{}/{}.log'.format(output_dir, output_prefix),
+ log=None,
+ report=None,
+ stdout=out,
+ **options)
+
+
+def parse_outputs(output_dir):
+ """Parse output xmls from all executed tests."""
+
+ outs = [os.path.join(output_dir, file_name)
+ for file_name in os.listdir(output_dir)
+ if file_name.endswith('.xml')]
+ # pylint: disable=star-args
+ robot.rebot(*outs, merge=True)
+
+
+def topology_lookup(topology_paths, topo_dir, validate):
+ """Make topology list and validate topologies against schema
+
+ :param parsed_args: topology list, is empty then scans topologies in
+ topo_dir
+ :param topo_dir: scan directory for topologies
+ :param validate: if True then validate topology
+ :return: list of topologies
+ """
+
+ ret_topologies = []
+ if topology_paths:
+ for topo in topology_paths:
+ if os.path.exists(topo):
+ ret_topologies.append(topo)
+ else:
+ print("Topology file {} doesn't exist".format(topo),
+ file=sys.stderr)
+ else:
+ ret_topologies = [os.path.join(topo_dir, file_name)
+ for file_name in os.listdir(topo_dir)
+ if file_name.lower().endswith('.yaml')]
+
+ if len(ret_topologies) == 0:
+ print('No valid topology found', file=sys.stderr)
+ exit(1)
+
+ # validate topologies against schema
+ exit_on_error = False
+ for topology_name in ret_topologies:
+ try:
+ with open(topology_name) as file_name:
+ yaml_obj = load(file_name)
+ core = Core(source_file=topology_name,
+ schema_files=yaml_obj["metadata"]["schema"])
+ core.validate()
+ except PyKwalifyException as ex:
+ print('Unable to verify topology {}, schema error: {}'.\
+ format(topology_name, ex),
+ file=sys.stderr)
+ exit_on_error = True
+ except KeyError as ex:
+ print('Unable to verify topology {}, key error: {}'.\
+ format(topology_name, ex),
+ file=sys.stderr)
+ exit_on_error = True
+ except Exception as ex:
+ print('Unable to verify topology {}, {}'.format(topology_name, ex),
+ file=sys.stderr)
+ exit_on_error = True
+
+ if exit_on_error and validate:
+ exit(1)
+
+ return ret_topologies
+
+
+def main():
+ """Main function."""
+ parser = argparse.ArgumentParser(description='A test runner')
+ parser.add_argument('-i', '--include', action='append',
+ help='include tests with tag')
+ parser.add_argument('-e', '--exclude', action='append',
+ help='exclude tests with tag')
+ parser.add_argument('-s', '--suite', action='append',
+ help='full name of suite to run')
+ parser.add_argument('-t', '--topology', action='append',
+ help='topology where tests should be run')
+ parser.add_argument('-d', '--test_dir', nargs='?', default=TESTS_DIR,
+ help='where tests are stored')
+ parser.add_argument('-o', '--output_dir', nargs='?', default=OUTPUTS_DIR,
+ help='where results are stored')
+ parser.add_argument('-L', '--loglevel', nargs='?', default='INFO', type=str,
+ choices=['TRACE', 'DEBUG', 'INFO', 'WARN', 'NONE'],
+ help='robot frameworks level for logging')
+ parser.add_argument('-n', '--no_validate', action="store_false",
+ help='Do not exit if topology validation failed')
+
+ args = parser.parse_args()
+
+ i = args.include or []
+ excl = args.exclude or []
+ suite_filter = args.suite or []
+ test_dir = args.test_dir
+
+ # prepare output subdir
+ suite_output_dir = os.path.join(args.output_dir,
+ time.strftime('%y%m%d%H%M%S'))
+ os.makedirs(suite_output_dir)
+
+ topologies = topology_lookup(args.topology, TOPOLOGIES_DIR,
+ args.no_validate)
+ suite_list = get_suite_list(test_dir, include=i, exclude=excl,
+ suite=suite_filter)
+
+ # TODO: do the topology suite mapping magic
+ # for now all tests on single topology
+ if len(topologies) > 1:
+ print('Multiple topologies unsupported yet', file=sys.stderr)
+ exit(1)
+ topology_suite_mapping = {topologies[0]: suite_list}
+
+ # on all topologies, run test
+ # TODO: run parallel
+ for topology_path, topology_suite_list in topology_suite_mapping.items():
+ topology_path_variable = 'TOPOLOGY_PATH:{}'.format(topology_path)
+ variables = [topology_path_variable]
+ print('Runing tests on topology {}'.format(topology_path))
+ run_suites(test_dir, topology_suite_list, variable=variables,
+ output_dir=suite_output_dir,
+ output_prefix=''.join(sample(ascii_lowercase, 5)),
+ include=i, exclude=excl, loglevel=args.loglevel)
+
+ print('Parsing test results')
+ parse_outputs(suite_output_dir)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pylint.cfg b/pylint.cfg
new file mode 100644
index 0000000000..37622580ac
--- /dev/null
+++ b/pylint.cfg
@@ -0,0 +1,280 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+#disable=
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=parseable
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+# List of optional constructs for which whitespace checking is disabled
+no-space-check=trailing-comma,dict-separator
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_$|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct attribute names in class
+# bodies
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=__.*__
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000..6146c4b144
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,9 @@
+robotframework==2.9.2
+paramiko==1.16.0
+scp==0.10.2
+ipaddress==1.0.16
+interruptingcow==0.6
+PyYAML==3.11
+pykwalify==1.5.0
+scapy==2.3.1
+
diff --git a/resources/__init__.py b/resources/__init__.py
new file mode 100644
index 0000000000..83c9fbff9a
--- /dev/null
+++ b/resources/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""
+__init__ file for directory resources
+"""
diff --git a/resources/libraries/__init__.py b/resources/libraries/__init__.py
new file mode 100644
index 0000000000..73d02f6793
--- /dev/null
+++ b/resources/libraries/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""
+__init__ file for directory resources/libraries
+"""
diff --git a/resources/libraries/bash/dut_setup.sh b/resources/libraries/bash/dut_setup.sh
new file mode 100644
index 0000000000..dc36a08f85
--- /dev/null
+++ b/resources/libraries/bash/dut_setup.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+echo
+echo Restart VPP
+echo
+sudo -S service vpp restart
+
+echo
+echo List vpp packages
+echo
+dpkg -l vpp\*
+
+echo
+echo List /proc/meminfo
+echo
+cat /proc/meminfo
+
+echo
+echo See vpe process
+echo
+ps aux | grep vpe
+
+echo
+echo See free memory
+echo
+free -m
+
diff --git a/resources/libraries/python/DUTSetup.py b/resources/libraries/python/DUTSetup.py
new file mode 100644
index 0000000000..76f76aef7e
--- /dev/null
+++ b/resources/libraries/python/DUTSetup.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 robot.api import logger
+from topology import NodeType
+from ssh import SSH
+from constants import Constants
+
+class DUTSetup(object):
+
+ def __init__(self):
+ pass
+
+ def setup_all_duts(self, nodes):
+ """Prepare all DUTs in given topology for test execution."""
+ for node in nodes.values():
+ if node['type'] == NodeType.DUT:
+ self.setup_dut(node)
+
+ def setup_dut(self, node):
+ ssh = SSH()
+ ssh.connect(node)
+
+ (ret_code, stdout, stderr) = \
+ ssh.exec_command('sudo -Sn bash {0}/{1}/dut_setup.sh'.format(
+ Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH))
+ logger.trace(stdout)
+ if 0 != int(ret_code):
+ logger.error('DUT {0} setup script failed: "{1}"'.
+ format(node['host'], stdout + stderr))
+ raise Exception('DUT test setup script failed at node {}'.
+ format(node['host']))
diff --git a/resources/libraries/python/IPUtil.py b/resources/libraries/python/IPUtil.py
new file mode 100644
index 0000000000..3e002b3495
--- /dev/null
+++ b/resources/libraries/python/IPUtil.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Common IP utilities library."""
+
+from ssh import SSH
+from constants import Constants
+
+
+class IPUtil(object):
+ """Common IP utilities"""
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def vpp_ip_probe(node, interface, addr):
+ """Run ip probe on VPP node.
+
+ Args:
+ node (Dict): VPP node.
+ interface (str): Interface name
+ addr (str): IPv4/IPv6 address
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = "{c}".format(c=Constants.VAT_BIN_NAME)
+ cmd_input = 'exec ip probe {dev} {ip}'.format(dev=interface, ip=addr)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception('VPP ip probe {dev} {ip} failed on {h}'.format(
+ dev=interface, ip=addr, h=node['host']))
diff --git a/resources/libraries/python/IPv4NodeAddress.py b/resources/libraries/python/IPv4NodeAddress.py
new file mode 100644
index 0000000000..0f2c1d9cd3
--- /dev/null
+++ b/resources/libraries/python/IPv4NodeAddress.py
@@ -0,0 +1,104 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Robot framework variable file.
+
+ Create dictionary variable nodes_ipv4_addr of IPv4 addresses from
+ available networks.
+"""
+from ipaddress import IPv4Network
+
+# Default list of IPv4 subnets
+IPV4_NETWORKS = ['192.168.1.0/24',
+ '192.168.2.0/24',
+ '192.168.3.0/24']
+
+
+class IPv4NetworkGenerator(object):
+ """IPv4 network generator."""
+ def __init__(self, networks):
+ """
+ :param networks: list of strings containing IPv4 subnet
+ with prefix length
+ """
+ self._networks = list()
+ for network in networks:
+ net = IPv4Network(unicode(network))
+ subnet, _ = network.split('/')
+ self._networks.append((net, subnet))
+ if len(self._networks) == 0:
+ raise Exception('No IPv4 networks')
+
+ def next_network(self):
+ """
+ :return: next network in form (IPv4Network, subnet)
+ """
+ if len(self._networks):
+ return self._networks.pop()
+ else:
+ raise StopIteration()
+
+
+def get_variables(networks=IPV4_NETWORKS[:]):
+ """
+ Create dictionary of IPv4 addresses generated from provided subnet list.
+
+ Example of returned dictionary:
+ network = {
+ 'NET1': {
+ 'subnet': '192.168.1.0',
+ 'prefix': 24,
+ 'port1': {
+ 'addr': '192.168.1.1',
+ },
+ 'port2': {
+ 'addr': '192.168.1.0',
+ },
+ },
+ 'NET2': {
+ 'subnet': '192.168.2.0',
+ 'prefix': 24,
+ 'port1': {
+ 'addr': '192.168.2.1',
+ },
+ 'port2': {
+ 'addr': '192.168.2.2',
+ },
+ },
+ }
+
+ This function is called by RobotFramework automatically.
+
+ :param networks: list of subnets in form a.b.c.d/length
+ :return: Dictionary of IPv4 addresses
+ """
+ net_object = IPv4NetworkGenerator(networks)
+
+ network = {}
+ interface_count_per_node = 2
+
+ for subnet_num in range(len(networks)):
+ net, net_str = net_object.next_network()
+ key = 'NET{}'.format(subnet_num + 1)
+ network[key] = {
+ 'subnet': net_str,
+ 'prefix': net.prefixlen,
+ }
+ hosts = net.hosts()
+ for port_num in range(interface_count_per_node):
+ port = 'port{}'.format(port_num + 1)
+ network[key][port] = {
+ 'addr': str(next(hosts)),
+ }
+
+ return {'DICT__nodes_ipv4_addr': network}
diff --git a/resources/libraries/python/IPv4Util.py b/resources/libraries/python/IPv4Util.py
new file mode 100644
index 0000000000..5480bfcea3
--- /dev/null
+++ b/resources/libraries/python/IPv4Util.py
@@ -0,0 +1,499 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Implements IPv4 RobotFramework keywords"""
+
+from socket import inet_ntoa
+from struct import pack
+from abc import ABCMeta, abstractmethod
+import copy
+
+from robot.api import logger as log
+from robot.api.deco import keyword
+from robot.utils.asserts import assert_not_equal
+
+import resources.libraries.python.ssh as ssh
+from resources.libraries.python.topology import Topology
+from resources.libraries.python.topology import NodeType
+from resources.libraries.python.VatExecutor import VatExecutor
+from resources.libraries.python.TrafficScriptExecutor\
+ import TrafficScriptExecutor
+
+
+class IPv4Node(object):
+ """Abstract class of a node in a topology."""
+ __metaclass__ = ABCMeta
+
+ def __init__(self, node_info):
+ self.node_info = node_info
+
+ @staticmethod
+ def _get_netmask(prefix_length):
+ bits = 0xffffffff ^ (1 << 32 - prefix_length) - 1
+ return inet_ntoa(pack('>I', bits))
+
+ @abstractmethod
+ def set_ip(self, interface, address, prefix_length):
+ """Configure IPv4 address on interface
+ :param interface: interface name
+ :param address:
+ :param prefix_length:
+ :type interface: str
+ :type address: str
+ :type prefix_length: int
+ :return: nothing
+ """
+ pass
+
+ @abstractmethod
+ def set_interface_state(self, interface, state):
+ """Set interface state
+ :param interface: interface name string
+ :param state: one of following values: "up" or "down"
+ :return: nothing
+ """
+ pass
+
+ @abstractmethod
+ def set_route(self, network, prefix_length, gateway, interface):
+ """Configure IPv4 route
+ :param network: network IPv4 address
+ :param prefix_length: mask length
+ :param gateway: IPv4 address of the gateway
+ :param interface: interface name
+ :type network: str
+ :type prefix_length: int
+ :type gateway: str
+ :type interface: str
+ :return: nothing
+ """
+ pass
+
+ @abstractmethod
+ def unset_route(self, network, prefix_length, gateway, interface):
+ """Remove specified IPv4 route
+ :param network: network IPv4 address
+ :param prefix_length: mask length
+ :param gateway: IPv4 address of the gateway
+ :param interface: interface name
+ :type network: str
+ :type prefix_length: int
+ :type gateway: str
+ :type interface: str
+ :return: nothing
+ """
+ pass
+
+ @abstractmethod
+ def flush_ip_addresses(self, interface):
+ """Flush all IPv4 addresses from specified interface
+ :param interface: interface name
+ :type interface: str
+ :return: nothing
+ """
+ pass
+
+ @abstractmethod
+ def ping(self, destination_address, source_interface):
+ """Send an ICMP request to destination node
+ :param destination_address: address to send the ICMP request
+ :param source_interface:
+ :type destination_address: str
+ :type source_interface: str
+ :return: nothing
+ """
+ pass
+
+
+class Tg(IPv4Node):
+ """Traffic generator node"""
+ def __init__(self, node_info):
+ super(Tg, self).__init__(node_info)
+
+ def _execute(self, cmd):
+ return ssh.exec_cmd_no_error(self.node_info, cmd)
+
+ def _sudo_execute(self, cmd):
+ return ssh.exec_cmd_no_error(self.node_info, cmd, sudo=True)
+
+ def set_ip(self, interface, address, prefix_length):
+ cmd = 'ip -4 addr flush dev {}'.format(interface)
+ self._sudo_execute(cmd)
+ cmd = 'ip addr add {}/{} dev {}'.format(address, prefix_length,
+ interface)
+ self._sudo_execute(cmd)
+
+ # TODO: not ipv4-specific, move to another class
+ def set_interface_state(self, interface, state):
+ cmd = 'ip link set {} {}'.format(interface, state)
+ self._sudo_execute(cmd)
+
+ def set_route(self, network, prefix_length, gateway, interface):
+ netmask = self._get_netmask(prefix_length)
+ cmd = 'route add -net {} netmask {} gw {}'.\
+ format(network, netmask, gateway)
+ self._sudo_execute(cmd)
+
+ def unset_route(self, network, prefix_length, gateway, interface):
+ self._sudo_execute('ip route delete {}/{}'.
+ format(network, prefix_length))
+
+ def arp_ping(self, destination_address, source_interface):
+ self._sudo_execute('arping -c 1 -I {} {}'.format(source_interface,
+ destination_address))
+
+ def ping(self, destination_address, source_interface):
+ self._execute('ping -c 1 -w 5 -I {} {}'.format(source_interface,
+ destination_address))
+
+ def flush_ip_addresses(self, interface):
+ self._sudo_execute('ip addr flush dev {}'.format(interface))
+
+
+class Dut(IPv4Node):
+ """Device under test"""
+ def __init__(self, node_info):
+ super(Dut, self).__init__(node_info)
+
+ def get_sw_if_index(self, interface):
+ """Get sw_if_index of specified interface from current node
+ :param interface: interface name
+ :type interface: str
+ :return: sw_if_index of 'int' type
+ """
+ return Topology().get_interface_sw_index(self.node_info, interface)
+
+ def exec_vat(self, script, **args):
+ """Wrapper for VAT executor.
+ :param script: script to execute
+ :param args: parameters to the script
+ :type script: str
+ :type args: dict
+ :return: nothing
+ """
+ # TODO: check return value
+ VatExecutor.cmd_from_template(self.node_info, script, **args)
+
+ def set_ip(self, interface, address, prefix_length):
+ self.exec_vat('add_ip_address.vat',
+ sw_if_index=self.get_sw_if_index(interface),
+ address=address, prefix_length=prefix_length)
+
+ def set_interface_state(self, interface, state):
+ if state == 'up':
+ state = 'admin-up link-up'
+ elif state == 'down':
+ state = 'admin-down link-down'
+ else:
+ raise Exception('Unexpected interface state: {}'.format(state))
+
+ self.exec_vat('set_if_state.vat',
+ sw_if_index=self.get_sw_if_index(interface), state=state)
+
+ def set_route(self, network, prefix_length, gateway, interface):
+ sw_if_index = self.get_sw_if_index(interface)
+ self.exec_vat('add_route.vat',
+ network=network, prefix_length=prefix_length,
+ gateway=gateway, sw_if_index=sw_if_index)
+
+ def unset_route(self, network, prefix_length, gateway, interface):
+ self.exec_vat('del_route.vat', network=network,
+ prefix_length=prefix_length, gateway=gateway,
+ sw_if_index=self.get_sw_if_index(interface))
+
+ def arp_ping(self, destination_address, source_interface):
+ pass
+
+ def flush_ip_addresses(self, interface):
+ self.exec_vat('flush_ip_addresses.vat',
+ sw_if_index=self.get_sw_if_index(interface))
+
+ def ping(self, destination_address, source_interface):
+ pass
+
+
+def get_node(node_info):
+ """Creates a class instance derived from Node based on type.
+ :param node_info: dictionary containing information on nodes in topology
+ :return: Class instance that is derived from Node
+ """
+ if node_info['type'] == NodeType.TG:
+ return Tg(node_info)
+ elif node_info['type'] == NodeType.DUT:
+ return Dut(node_info)
+ else:
+ raise NotImplementedError('Node type "{}" unsupported!'.
+ format(node_info['type']))
+
+
+def get_node_hostname(node_info):
+ """Get string identifying specifed node.
+ :param node_info: Node in the topology.
+ :type node_info: Dict
+ :return: String identifying node.
+ """
+ return node_info['host']
+
+
+class IPv4Util(object):
+ """Implements keywords for IPv4 tests."""
+
+ ADDRESSES = {} # holds configured IPv4 addresses
+ PREFIXES = {} # holds configured IPv4 addresses' prefixes
+ SUBNETS = {} # holds configured IPv4 addresses' subnets
+
+ """
+ Helper dictionary used when setting up ipv4 addresses in topology
+
+ Example value:
+ 'link1': { b'port1': {b'addr': b'192.168.3.1'},
+ b'port2': {b'addr': b'192.168.3.2'},
+ b'prefix': 24,
+ b'subnet': b'192.168.3.0'}
+ """
+ topology_helper = None
+
+ @staticmethod
+ def next_address(subnet):
+ """Get next unused IPv4 address from a subnet
+ :param subnet: holds available IPv4 addresses
+ :return: tuple (ipv4_address, prefix_length)
+ """
+ for i in range(1, 4):
+ # build a key and try to get it from address dictionary
+ interface = 'port{}'.format(i)
+ if interface in subnet:
+ addr = subnet[interface]['addr']
+ del subnet[interface]
+ return addr, subnet['prefix']
+ raise Exception('Not enough ipv4 addresses in subnet')
+
+ @staticmethod
+ def next_network(nodes_addr):
+ """Get next unused network from dictionary
+ :param nodes_addr: dictionary of available networks
+ :return: dictionary describing an IPv4 subnet with addresses
+ """
+ assert_not_equal(len(nodes_addr), 0, 'Not enough networks')
+ _, subnet = nodes_addr.popitem()
+ return subnet
+
+ @staticmethod
+ def configure_ipv4_addr_on_node(node, nodes_addr):
+ """Configure IPv4 address for all interfaces on a node in topology
+ :param node: dictionary containing information about node
+ :param nodes_addr: dictionary containing IPv4 addresses
+ :return:
+ """
+ for interface, interface_data in node['interfaces'].iteritems():
+ if interface == 'mgmt':
+ continue
+ if interface_data['link'] not in IPv4Util.topology_helper:
+ IPv4Util.topology_helper[interface_data['link']] = \
+ IPv4Util.next_network(nodes_addr)
+
+ network = IPv4Util.topology_helper[interface_data['link']]
+ address, prefix = IPv4Util.next_address(network)
+
+ get_node(node).set_ip(interface_data['name'], address, prefix)
+ key = (get_node_hostname(node), interface_data['name'])
+ IPv4Util.ADDRESSES[key] = address
+ IPv4Util.PREFIXES[key] = prefix
+ IPv4Util.SUBNETS[key] = network['subnet']
+
+ @staticmethod
+ def nodes_setup_ipv4_addresses(nodes_info, nodes_addr):
+ """Configure IPv4 addresses on all non-management interfaces for each
+ node in nodes_info
+ :param nodes_info: dictionary containing information on all nodes
+ in topology
+ :param nodes_addr: dictionary containing IPv4 addresses
+ :return: nothing
+ """
+ IPv4Util.topology_helper = {}
+ # make a deep copy of nodes_addr because of modifications
+ nodes_addr_copy = copy.deepcopy(nodes_addr)
+ for _, node in nodes_info.iteritems():
+ IPv4Util.configure_ipv4_addr_on_node(node, nodes_addr_copy)
+
+ @staticmethod
+ def nodes_clear_ipv4_addresses(nodes):
+ """Clear all addresses from all nodes in topology
+ :param nodes: dictionary containing information on all nodes
+ :return: nothing
+ """
+ for _, node in nodes.iteritems():
+ for interface, interface_data in node['interfaces'].iteritems():
+ if interface == 'mgmt':
+ continue
+ IPv4Util.flush_ip_addresses(interface_data['name'], node)
+
+ # TODO: not ipv4-specific, move to another class
+ @staticmethod
+ @keyword('Node "${node}" interface "${interface}" is in "${state}" state')
+ def set_interface_state(node, interface, state):
+ """See IPv4Node.set_interface_state for more information.
+ :param node:
+ :param interface:
+ :param state:
+ :return:
+ """
+ log.debug('Node {} interface {} is in {} state'.format(
+ get_node_hostname(node), interface, state))
+ get_node(node).set_interface_state(interface, state)
+
+ @staticmethod
+ @keyword('Node "${node}" interface "${port}" has IPv4 address '
+ '"${address}" with prefix length "${prefix_length}"')
+ def set_interface_address(node, interface, address, length):
+ """See IPv4Node.set_ip for more information.
+ :param node:
+ :param interface:
+ :param address:
+ :param length:
+ :return:
+ """
+ log.debug('Node {} interface {} has IPv4 address {} with prefix '
+ 'length {}'.format(get_node_hostname(node), interface,
+ address, length))
+ get_node(node).set_ip(interface, address, int(length))
+ hostname = get_node_hostname(node)
+ IPv4Util.ADDRESSES[hostname, interface] = address
+ IPv4Util.PREFIXES[hostname, interface] = int(length)
+ # TODO: Calculate subnet from ip address and prefix length.
+ # IPv4Util.SUBNETS[hostname, interface] =
+
+ @staticmethod
+ @keyword('From node "${node}" interface "${port}" ARP-ping '
+ 'IPv4 address "${ip_address}"')
+ def arp_ping(node, interface, ip_address):
+ log.debug('From node {} interface {} ARP-ping IPv4 address {}'.
+ format(get_node_hostname(node), interface, ip_address))
+ get_node(node).arp_ping(ip_address, interface)
+
+ @staticmethod
+ @keyword('Node "${node}" routes to IPv4 network "${network}" with prefix '
+ 'length "${prefix_length}" using interface "${interface}" via '
+ '"${gateway}"')
+ def set_route(node, network, prefix_length, interface, gateway):
+ """See IPv4Node.set_route for more information.
+ :param node:
+ :param network:
+ :param prefix_length:
+ :param interface:
+ :param gateway:
+ :return:
+ """
+ log.debug('Node {} routes to network {} with prefix length {} '
+ 'via {} interface {}'.format(get_node_hostname(node),
+ network, prefix_length,
+ gateway, interface))
+ get_node(node).set_route(network, int(prefix_length),
+ gateway, interface)
+
+ @staticmethod
+ @keyword('Remove IPv4 route from "${node}" to network "${network}" with '
+ 'prefix length "${prefix_length}" interface "${interface}" via '
+ '"${gateway}"')
+ def unset_route(node, network, prefix_length, interface, gateway):
+ """See IPv4Node.unset_route for more information.
+ :param node:
+ :param network:
+ :param prefix_length:
+ :param interface:
+ :param gateway:
+ :return:
+ """
+ get_node(node).unset_route(network, prefix_length, gateway, interface)
+
+ @staticmethod
+ @keyword('After ping is sent from node "${src_node}" interface '
+ '"${src_port}" with destination IPv4 address of node '
+ '"${dst_node}" interface "${dst_port}" a ping response arrives '
+ 'and TTL is decreased by "${ttl_dec}"')
+ def send_ping(src_node, src_port, dst_node, dst_port, hops):
+ """Send IPv4 ping and wait for response.
+ :param src_node: Source node.
+ :param src_port: Source interface.
+ :param dst_node: Destination node.
+ :param dst_port: Destination interface.
+ :param hops: Number of hops between src_node and dst_node.
+ """
+ log.debug('After ping is sent from node "{}" interface "{}" '
+ 'with destination IPv4 address of node "{}" interface "{}" '
+ 'a ping response arrives and TTL is decreased by "${}"'.
+ format(get_node_hostname(src_node), src_port,
+ get_node_hostname(dst_node), dst_port, hops))
+ node = src_node
+ src_mac = Topology.get_interface_mac(src_node, src_port)
+ if dst_node['type'] == NodeType.TG:
+ dst_mac = Topology.get_interface_mac(src_node, src_port)
+ adj_int = Topology.get_adjacent_interface(src_node, src_port)
+ first_hop_mac = adj_int['mac_address']
+ src_ip = IPv4Util.get_ip_addr(src_node, src_port)
+ dst_ip = IPv4Util.get_ip_addr(dst_node, dst_port)
+ args = '--src_if "{}" --src_mac "{}" --first_hop_mac "{}" ' \
+ '--src_ip "{}" --dst_ip "{}" --hops "{}"'\
+ .format(src_port, src_mac, first_hop_mac, src_ip, dst_ip, hops)
+ if dst_node['type'] == NodeType.TG:
+ args += ' --dst_if "{}" --dst_mac "{}"'.format(dst_port, dst_mac)
+ TrafficScriptExecutor.run_traffic_script_on_node(
+ "ipv4_ping_ttl_check.py", node, args)
+
+ @staticmethod
+ @keyword('Get IPv4 address of node "${node}" interface "${port}"')
+ def get_ip_addr(node, port):
+ """Get IPv4 address configured on specified interface
+ :param node: node dictionary
+ :param port: interface name
+ :return: IPv4 address of specified interface as a 'str' type
+ """
+ log.debug('Get IPv4 address of node {} interface {}'.
+ format(get_node_hostname(node), port))
+ return IPv4Util.ADDRESSES[(get_node_hostname(node), port)]
+
+ @staticmethod
+ @keyword('Get IPv4 address prefix of node "${node}" interface "${port}"')
+ def get_ip_addr_prefix(node, port):
+ """ Get IPv4 address prefix for specified interface.
+ :param node: Node dictionary.
+ :param port: Interface name.
+ """
+ log.debug('Get IPv4 address prefix of node {} interface {}'.
+ format(get_node_hostname(node), port))
+ return IPv4Util.PREFIXES[(get_node_hostname(node), port)]
+
+ @staticmethod
+ @keyword('Get IPv4 subnet of node "${node}" interface "${port}"')
+ def get_ip_addr_subnet(node, port):
+ """ Get IPv4 subnet of specified interface.
+ :param node: Node dictionary.
+ :param port: Interface name.
+ """
+ log.debug('Get IPv4 subnet of node {} interface {}'.
+ format(get_node_hostname(node), port))
+ return IPv4Util.SUBNETS[(get_node_hostname(node), port)]
+
+ @staticmethod
+ @keyword('Flush IPv4 addresses "${port}" "${node}"')
+ def flush_ip_addresses(port, node):
+ """See IPv4Node.flush_ip_addresses for more information.
+ :param port:
+ :param node:
+ :return:
+ """
+ key = (get_node_hostname(node), port)
+ del IPv4Util.ADDRESSES[key]
+ del IPv4Util.PREFIXES[key]
+ del IPv4Util.SUBNETS[key]
+ get_node(node).flush_ip_addresses(port)
diff --git a/resources/libraries/python/IPv6NodesAddr.py b/resources/libraries/python/IPv6NodesAddr.py
new file mode 100644
index 0000000000..33192b878f
--- /dev/null
+++ b/resources/libraries/python/IPv6NodesAddr.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Robot framework variable file.
+
+ Create dictionary variable nodes_ipv6_addr with IPv6 adresses from available
+ networks.
+"""
+
+from IPv6Setup import IPv6Networks
+from topology import Topology
+
+# Default list of available IPv6 networks
+IPV6_NETWORKS = ['db01::/64', 'db02::/64', 'db03::/64']
+
+
+def get_variables(nodes, networks=IPV6_NETWORKS):
+ """Special robot framework method that returns dictionary nodes_ipv6_addr,
+ mapping of node and interface name to IPv6 adddress.
+
+ :param nodes: Nodes of the test topology.
+ :param networks: list of available IPv6 networks
+ :type nodes: dict
+ :type networks: list
+
+ .. note::
+ Robot framework calls it automatically.
+ """
+ topo = Topology()
+ links = topo.get_links(nodes)
+
+ if len(links) > len(networks):
+ raise Exception('Not enough available IPv6 networks for topology.')
+
+ ip6_n = IPv6Networks(networks)
+
+ nets = {}
+
+ for link in links:
+ ip6_net = ip6_n.next_network()
+ net_hosts = ip6_net.hosts()
+ port_idx = 0
+ ports = {}
+ for node in nodes.values():
+ if_name = topo.get_interface_by_link_name(node, link)
+ if if_name is not None:
+ port = {'addr': str(next(net_hosts)),
+ 'node': node['host'],
+ 'if': if_name}
+ port_idx += 1
+ port_id = 'port{0}'.format(port_idx)
+ ports.update({port_id: port})
+ nets.update({link: {'net_addr': str(ip6_net.network_address),
+ 'prefix': ip6_net.prefixlen,
+ 'ports': ports}})
+
+ return {'DICT__nodes_ipv6_addr': nets}
diff --git a/resources/libraries/python/IPv6Setup.py b/resources/libraries/python/IPv6Setup.py
new file mode 100644
index 0000000000..45a8eba58d
--- /dev/null
+++ b/resources/libraries/python/IPv6Setup.py
@@ -0,0 +1,289 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Library to set up IPv6 in topology."""
+
+from ssh import SSH
+from ipaddress import IPv6Network
+from topology import NodeType
+from topology import Topology
+from constants import Constants
+
+
+class IPv6Networks(object):
+ """IPv6 network iterator.
+
+ :param networks: List of the available IPv6 networks.
+ :type networks: list
+ """
+ def __init__(self, networks):
+ self._networks = list()
+ for network in networks:
+ net = IPv6Network(unicode(network))
+ self._networks.append(net)
+ num = len(self._networks)
+ if num == 0:
+ raise Exception('No IPv6 networks')
+
+ def next_network(self):
+ """Get the next elemnt of the iterator.
+
+ :return: IPv6 network.
+ :rtype: IPv6Network object
+ :raises: StopIteration if there is no more elements.
+ """
+ if len(self._networks):
+ return self._networks.pop()
+ else:
+ raise StopIteration()
+
+
+class IPv6Setup(object):
+ """IPv6 setup in topology."""
+
+ def __init__(self):
+ pass
+
+ def nodes_setup_ipv6_addresses(self, nodes, nodes_addr):
+ """Setup IPv6 addresses on all VPP nodes in topology.
+
+ :param nodes: Nodes of the test topology.
+ :param nodes_addr: Available nodes IPv6 adresses.
+ :type nodes: dict
+ :type nodes_addr: dict
+ """
+ for net in nodes_addr.values():
+ for port in net['ports'].values():
+ host = port.get('node')
+ if host is None:
+ continue
+ topo = Topology()
+ node = topo.get_node_by_hostname(nodes, host)
+ if node is None:
+ continue
+ if node['type'] == NodeType.DUT:
+ self.vpp_set_if_ipv6_addr(node, port['if'], port['addr'],
+ net['prefix'])
+
+ def nodes_clear_ipv6_addresses(self, nodes, nodes_addr):
+ """Remove IPv6 addresses from all VPP nodes in topology.
+
+ :param nodes: Nodes of the test topology.
+ :param nodes_addr: Available nodes IPv6 adresses.
+ :type nodes: dict
+ :type nodes_addr: dict
+ """
+ for net in nodes_addr.values():
+ for port in net['ports'].values():
+ host = port.get('node')
+ if host is None:
+ continue
+ topo = Topology()
+ node = topo.get_node_by_hostname(nodes, host)
+ if node is None:
+ continue
+ if node['type'] == NodeType.DUT:
+ self.vpp_del_if_ipv6_addr(node, port['if'], port['addr'],
+ net['prefix'])
+
+ @staticmethod
+ def linux_set_if_ipv6_addr(node, interface, addr, prefix):
+ """Set IPv6 address on linux host.
+
+ :param node: Linux node.
+ :param interface: Node interface.
+ :param addr: IPv6 address.
+ :param prefix: IPv6 address prefix.
+ :type node: dict
+ :type interface: str
+ :type addr: str
+ :type prefix: str
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = "ifconfig {dev} inet6 add {ip}/{p} up".format(dev=interface,
+ ip=addr, p=prefix)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception('TG ifconfig failed')
+
+ @staticmethod
+ def linux_del_if_ipv6_addr(node, interface, addr, prefix):
+ """Delete IPv6 address on linux host.
+
+ :param node: Linux node.
+ :param interface: Node interface.
+ :param addr: IPv6 address.
+ :param prefix: IPv6 address prefix.
+ :type node: dict
+ :type interface: str
+ :type addr: str
+ :type prefix: str
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = "ifconfig {dev} inet6 del {ip}/{p}".format(dev=interface,
+ ip=addr,
+ p=prefix)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception('TG ifconfig failed')
+
+ cmd = "ifconfig {dev} down".format(dev=interface)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception('TG ifconfig failed')
+
+ @staticmethod
+ def vpp_set_if_ipv6_addr(node, interface, addr, prefix):
+ """Set IPv6 address on VPP.
+
+ :param node: VPP node.
+ :param interface: Node interface.
+ :param addr: IPv6 address.
+ :param prefix: IPv6 address prefix.
+ :type node: dict
+ :type interface: str
+ :type addr: str
+ :type prefix: str
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = '{c}'.format(c=Constants.VAT_BIN_NAME)
+ cmd_input = 'sw_interface_add_del_address {dev} {ip}/{p}'.format(
+ dev=interface, ip=addr, p=prefix)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception('VPP sw_interface_add_del_address failed on {h}'
+ .format(h=node['host']))
+
+ cmd_input = 'sw_interface_set_flags {dev} admin-up'.format(
+ dev=interface)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception('VPP sw_interface_set_flags failed on {h}'.format(
+ h=node['host']))
+
+ @staticmethod
+ def vpp_del_if_ipv6_addr(node, interface, addr, prefix):
+ """Delete IPv6 address on VPP.
+
+ :param node: VPP node.
+ :param interface: Node interface.
+ :param addr: IPv6 address.
+ :param prefix: IPv6 address prefix.
+ :type node: dict
+ :type interface: str
+ :type addr: str
+ :type prefix: str
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = '{c}'.format(c=Constants.VAT_BIN_NAME)
+ cmd_input = 'sw_interface_add_del_address {dev} {ip}/{p} del'.format(
+ dev=interface, ip=addr, p=prefix)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception(
+ 'sw_interface_add_del_address failed on {h}'.
+ format(h=node['host']))
+
+ cmd_input = 'sw_interface_set_flags {dev} admin-down'.format(
+ dev=interface)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception('VPP sw_interface_set_flags failed on {h}'.format(
+ h=node['host']))
+
+ @staticmethod
+ def vpp_ra_supress_link_layer(node, interface):
+ """Supress ICMPv6 router advertisement message for link scope address
+
+ :param node: VPP node.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = '{c}'.format(c=Constants.VAT_BIN_NAME)
+ cmd_input = 'exec ip6 nd {0} ra-surpress-link-layer'.format(
+ interface)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd, cmd_input)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on {1}".format(cmd_input,
+ node['host']))
+
+ def vpp_all_ra_supress_link_layer(self, nodes):
+ """Supress ICMPv6 router advertisement message for link scope address
+ on all VPP nodes in the topology
+
+ :param nodes: Nodes of the test topology.
+ :type nodes: dict
+ """
+ for node in nodes.values():
+ if node['type'] == NodeType.TG:
+ continue
+ for port_k, port_v in node['interfaces'].items():
+ if port_k == 'mgmt':
+ continue
+ if_name = port_v.get('name')
+ if if_name is None:
+ continue
+ self.vpp_ra_supress_link_layer(node, if_name)
+
+ @staticmethod
+ def vpp_ipv6_route_add(node, link, interface, nodes_addr):
+ """Setup IPv6 route on the VPP node.
+
+ :param node: Node to add route on.
+ :param link: Route to following link.
+ :param interface: Route output interface.
+ :param nodes_addr: Available nodes IPv6 adresses.
+ :type node: dict
+ :type link: str
+ :type interface: str
+ :type nodes_addr: dict
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ # Get route destination address from link name
+ net = nodes_addr.get(link)
+ if net is None:
+ raise ValueError('No network for link "{0}"'.format(link))
+ dst_net = '{0}/{1}'.format(net['net_addr'], net['prefix'])
+
+ # Get next-hop address
+ nh_addr = None
+ for net in nodes_addr.values():
+ for port in net['ports'].values():
+ if port['if'] == interface and port['node'] == node['host']:
+ for nh in net['ports'].values():
+ if nh['if'] != interface and nh['node'] != node['host']:
+ nh_addr = nh['addr']
+ if nh_addr is None:
+ raise Exception('next-hop not found')
+
+ cmd_input = 'ip_add_del_route {0} via {1} {2} resolve-attempts 10'. \
+ format(dst_net, nh_addr, interface)
+ (ret_code, _, _) = ssh.exec_command_sudo(Constants.VAT_BIN_NAME,
+ cmd_input)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on {1}".format(cmd_input,
+ node['host']))
diff --git a/resources/libraries/python/IPv6Util.py b/resources/libraries/python/IPv6Util.py
new file mode 100644
index 0000000000..a96683b164
--- /dev/null
+++ b/resources/libraries/python/IPv6Util.py
@@ -0,0 +1,101 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""IPv6 utilities library."""
+
+import re
+from ssh import SSH
+
+
+class IPv6Util(object):
+ """IPv6 utilities"""
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def ipv6_ping(src_node, dst_addr, count=3, data_size=56, timeout=1):
+ """IPv6 ping.
+
+ Args:
+ src_node (Dict): Node where ping run.
+ dst_addr (str): Destination IPv6 address.
+ count (Optional[int]): Number of echo requests.
+ data_size (Optional[int]): Number of the data bytes.
+ timeout (Optional[int]): Time to wait for a response, in seconds.
+
+ Returns:
+ Number of lost packets.
+ """
+ ssh = SSH()
+ ssh.connect(src_node)
+
+ cmd = "ping6 -c {c} -s {s} -W {W} {dst}".format(c=count, s=data_size,
+ W=timeout,
+ dst=dst_addr)
+ (ret_code, stdout, _) = ssh.exec_command(cmd)
+
+ regex = re.compile(r'(\d+) packets transmitted, (\d+) received')
+ match = regex.search(stdout)
+ sent, received = match.groups()
+ packet_lost = int(sent) - int(received)
+
+ return packet_lost
+
+ @staticmethod
+ def ipv6_ping_port(nodes_ip, src_node, dst_node, port, cnt=3,
+ size=56, timeout=1):
+ """Send IPv6 ping to the node port.
+
+ Args:
+ nodes_ip (Dict): Nodes IPv6 adresses.
+ src_node (Dict): Node where ping run.
+ dst_node (Dict): Destination node.
+ port (str): Port on the destination node.
+ cnt (Optional[int]): Number of echo requests.
+ size (Optional[int]): Number of the data bytes.
+ timeout (Optional[int]): Time to wait for a response, in seconds.
+
+ Returns:
+ Number of lost packets.
+ """
+ dst_ip = IPv6Util.get_node_port_ipv6_address(dst_node, port, nodes_ip)
+ return IPv6Util.ipv6_ping(src_node, dst_ip, cnt, size, timeout)
+
+ @staticmethod
+ def get_node_port_ipv6_address(node, interface, nodes_addr):
+ """Return IPv6 address of the node port.
+
+ Args:
+ node (Dict): Node in the topology.
+ interface (str): Interface name of the node.
+ nodes_addr (Dict): Nodes IPv6 adresses.
+
+ Returns:
+ IPv6 address string.
+ """
+ for net in nodes_addr.values():
+ for port in net['ports'].values():
+ host = port.get('node')
+ dev = port.get('if')
+ if host == node['host'] and dev == interface:
+ ip = port.get('addr')
+ if ip is not None:
+ return ip
+ else:
+ raise Exception(
+ 'Node {n} port {p} IPv6 address is not set'.format(
+ n=node['host'], p=interface))
+
+ raise Exception('Node {n} port {p} IPv6 address not found.'.format(
+ n=node['host'], p=interface))
diff --git a/resources/libraries/python/InterfaceSetup.py b/resources/libraries/python/InterfaceSetup.py
new file mode 100644
index 0000000000..9b6043545a
--- /dev/null
+++ b/resources/libraries/python/InterfaceSetup.py
@@ -0,0 +1,152 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Interface setup library."""
+
+from ssh import SSH
+
+
+class InterfaceSetup(object):
+ """Interface setup utilities."""
+
+ __UDEV_IF_RULES_FILE = '/etc/udev/rules.d/10-network.rules'
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def tg_set_interface_driver(node, pci_addr, driver):
+ """Set interface driver on the TG node.
+
+ :param node: Node to set interface driver on (must be TG node).
+ :param interface: Interface name.
+ :param driver: Driver name.
+ :type node: dict
+ :type interface: str
+ :type driver: str
+ """
+ old_driver = InterfaceSetup.tg_get_interface_driver(node, pci_addr)
+ if old_driver == driver:
+ return
+
+ ssh = SSH()
+ ssh.connect(node)
+
+ # Unbind from current driver
+ if old_driver is not None:
+ cmd = 'sh -c "echo {0} > /sys/bus/pci/drivers/{1}/unbind"'.format(
+ pci_addr, old_driver)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on '{1}'".format(cmd,
+ node['host']))
+
+ # Bind to the new driver
+ cmd = 'sh -c "echo {0} > /sys/bus/pci/drivers/{1}/bind"'.format(
+ pci_addr, driver)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on '{1}'".format(cmd, node['host']))
+
+ @staticmethod
+ def tg_get_interface_driver(node, pci_addr):
+ """Get interface driver from the TG node.
+
+ :param node: Node to get interface driver on (must be TG node).
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Interface driver or None if not found.
+ :rtype: str
+
+ .. note::
+ # lspci -vmmks 0000:00:05.0
+ Slot: 00:05.0
+ Class: Ethernet controller
+ Vendor: Red Hat, Inc
+ Device: Virtio network device
+ SVendor: Red Hat, Inc
+ SDevice: Device 0001
+ PhySlot: 5
+ Driver: virtio-pci
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = 'lspci -vmmks {0}'.format(pci_addr)
+
+ (ret_code, stdout, _) = ssh.exec_command(cmd)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on '{1}'".format(cmd, node['host']))
+
+ for line in stdout.splitlines():
+ if len(line) == 0:
+ continue
+ (name, value) = line.split("\t", 1)
+ if name == 'Driver:':
+ return value
+
+ return None
+
+ @staticmethod
+ def tg_set_interfaces_udev_rules(node):
+ """Set udev rules for interfaces.
+
+ Create udev rules file in /etc/udev/rules.d where are rules for each
+ interface used by TG node, based on MAC interface has specific name.
+ So after unbind and bind again to kernel driver interface has same
+ name as before. This must be called after TG has set name for each
+ port in topology dictionary.
+ udev rule example
+ SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="52:54:00:e1:8a:0f",
+ NAME="eth1"
+
+ :param node: Node to set udev rules on (must be TG node).
+ :type node: dict
+ """
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = 'rm -f {0}'.format(InterfaceSetup.__UDEV_IF_RULES_FILE)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on '{1}'".format(cmd, node['host']))
+
+ for if_k, if_v in node['interfaces'].items():
+ if if_k == 'mgmt':
+ continue
+ rule = 'SUBSYSTEM==\\"net\\", ACTION==\\"add\\", ATTR{address}' + \
+ '==\\"' + if_v['mac_address'] + '\\", NAME=\\"' + \
+ if_v['name'] + '\\"'
+ cmd = 'sh -c "echo \'{0}\' >> {1}"'.format(
+ rule, InterfaceSetup.__UDEV_IF_RULES_FILE)
+ (ret_code, _, _) = ssh.exec_command_sudo(cmd)
+ if int(ret_code) != 0:
+ raise Exception("'{0}' failed on '{1}'".format(cmd,
+ node['host']))
+
+ cmd = '/etc/init.d/udev restart'
+ ssh.exec_command_sudo(cmd)
+
+ @staticmethod
+ def tg_set_interfaces_default_driver(node):
+ """Set interfaces default driver specified in topology yaml file.
+
+ :param node: Node to setup interfaces driver on (must be TG node).
+ :type node: dict
+ """
+ for if_k, if_v in node['interfaces'].items():
+ if if_k == 'mgmt':
+ continue
+ InterfaceSetup.tg_set_interface_driver(node, if_v['pci_address'],
+ if_v['driver'])
diff --git a/resources/libraries/python/PacketVerifier.py b/resources/libraries/python/PacketVerifier.py
new file mode 100644
index 0000000000..81798e1f68
--- /dev/null
+++ b/resources/libraries/python/PacketVerifier.py
@@ -0,0 +1,310 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""PacketVerifier module.
+
+ :Example:
+
+ >>> from scapy.all import *
+ >>> from PacketVerifier import *
+ >>> rxq = RxQueue('eth1')
+ >>> txq = TxQueue('eth1')
+ >>> src_mac = "AA:BB:CC:DD:EE:FF"
+ >>> dst_mac = "52:54:00:ca:5d:0b"
+ >>> src_ip = "11.11.11.10"
+ >>> dst_ip = "11.11.11.11"
+ >>> sent_packets = []
+ >>> pkt_send = Ether(src=src_mac, dst=dst_mac) /
+ ... IP(src=src_ip, dst=dst_ip) /
+ ... ICMP()
+ >>> sent_packets.append(pkt_send)
+ >>> txq.send(pkt_send)
+ >>> pkt_send = Ether(src=src_mac, dst=dst_mac) /
+ ... ARP(hwsrc=src_mac, psrc=src_ip, hwdst=dst_mac, pdst=dst_ip, op=2)
+ >>> sent_packets.append(pkt_send)
+ >>> txq.send(pkt_send)
+ >>> rxq.recv(100, sent_packets).show()
+ ###[ Ethernet ]###
+ dst = aa:bb:cc:dd:ee:ff
+ src = 52:54:00:ca:5d:0b
+ type = 0x800
+ ###[ IP ]###
+ version = 4L
+ ihl = 5L
+ tos = 0x0
+ len = 28
+ id = 43183
+ flags =
+ frag = 0L
+ ttl = 64
+ proto = icmp
+ chksum = 0xa607
+ src = 11.11.11.11
+ dst = 11.11.11.10
+ \options \
+ ###[ ICMP ]###
+ type = echo-reply
+ code = 0
+ chksum = 0xffff
+ id = 0x0
+ seq = 0x0
+ ###[ Padding ]###
+ load = 'RT\x00\xca]\x0b\xaa\xbb\xcc\xdd\xee\xff\x08\x06\x00\x01\x08\x00'
+ >>> rxq._proc.terminate()
+"""
+
+
+import socket
+import os
+import time
+from multiprocessing import Queue, Process
+from scapy.all import ETH_P_IP, ETH_P_IPV6, ETH_P_ALL, ETH_P_ARP
+from scapy.all import Ether, ARP, Packet
+from scapy.layers.inet6 import IPv6
+
+__all__ = ['RxQueue', 'TxQueue', 'Interface', 'create_gratuitous_arp_request',
+ 'auto_pad']
+
+# TODO: http://stackoverflow.com/questions/320232/ensuring-subprocesses-are-dead-on-exiting-python-program
+
+class PacketVerifier(object):
+ """Base class for TX and RX queue objects for packet verifier."""
+ def __init__(self, interface_name):
+ os.system('sudo echo 1 > /proc/sys/net/ipv6/conf/{0}/disable_ipv6'
+ .format(interface_name))
+ os.system('sudo ip link set {0} up promisc on'.format(interface_name))
+ self._sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
+ ETH_P_ALL)
+ self._sock.bind((interface_name, ETH_P_ALL))
+
+
+def extract_one_packet(buf):
+ """Extract one packet from the incoming buf buffer.
+
+ Takes string as input and looks for first whole packet in it.
+ If it finds one, it returns substring from the buf parameter.
+
+ :param buf: string representation of incoming packet buffer.
+ :type buf: string
+ :return: String representation of first packet in buf.
+ :rtype: string
+ """
+ pkt_len = 0
+
+ if len(buf) < 60:
+ return None
+
+ # print
+ # print buf.__repr__()
+ # print Ether(buf).__repr__()
+ # print len(Ether(buf))
+ # print
+ try:
+ ether_type = Ether(buf[0:14]).type
+ except AttributeError:
+ raise RuntimeError(
+ 'No EtherType in packet {0}'.format(buf.__repr__()))
+
+ if ether_type == ETH_P_IP:
+ # 14 is Ethernet fame header size.
+ # 4 bytes is just enough to look for length in ip header.
+ # ip total length contains just the IP packet length so add the Ether
+ # header.
+ pkt_len = Ether(buf[0:14+4]).len + 14
+ if len(buf) < 60:
+ return None
+ elif ether_type == ETH_P_IPV6:
+ if not Ether(buf[0:14+6]).haslayer(IPv6):
+ raise RuntimeError(
+ 'Invalid IPv6 packet {0}'.format(buf.__repr__()))
+ # ... to add to the above, 40 bytes is the length of IPV6 header.
+ # The ipv6.len only contains length of the payload and not the header
+ pkt_len = Ether(buf)['IPv6'].plen + 14 + 40
+ if len(buf) < 60:
+ return None
+ elif ether_type == ETH_P_ARP:
+ pkt = Ether(buf[:20])
+ if not pkt.haslayer(ARP):
+ raise RuntimeError('Incomplete ARP packet')
+ # len(eth) + arp(2 hw addr type + 2 proto addr type
+ # + 1b len + 1b len + 2b operation)
+
+ pkt_len = 14 + 8
+ pkt_len += 2 * pkt.getlayer(ARP).hwlen
+ pkt_len += 2 * pkt.getlayer(ARP).plen
+
+ del pkt
+ elif ether_type == 32821: # RARP (Reverse ARP)
+ pkt = Ether(buf[:20])
+ pkt.type = ETH_P_ARP # Change to ARP so it works with scapy
+ pkt = Ether(str(pkt))
+ if not pkt.haslayer(ARP):
+ pkt.show()
+ raise RuntimeError('Incomplete RARP packet')
+
+ # len(eth) + arp(2 hw addr type + 2 proto addr type
+ # + 1b len + 1b len + 2b operation)
+ pkt_len = 14 + 8
+ pkt_len += 2 * pkt.getlayer(ARP).hwlen
+ pkt_len += 2 * pkt.getlayer(ARP).plen
+
+ del pkt
+ else:
+ raise RuntimeError('Unknown protocol {0}'.format(ether_type))
+
+ if pkt_len < 60:
+ pkt_len = 60
+
+ if len(buf) < pkt_len:
+ return None
+
+ return buf[0:pkt_len]
+
+
+def packet_reader(interface_name, queue):
+ """Sub-process routine that reads packets and puts them to queue.
+
+ This function is meant to be run in separate subprocess and is in tight
+ loop reading raw packets from interface passed as parameter.
+
+ :param interace_name: Name of interface to read packets from.
+ :param queue: Queue in which this function will push incoming packets.
+ :type interface_name: string
+ :type queue: multiprocessing.Queue
+ :return: None
+ """
+ sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
+ sock.bind((interface_name, ETH_P_ALL))
+
+ buf = ""
+ while True:
+ recvd = sock.recv(1500)
+ buf = buf + recvd
+
+ pkt = extract_one_packet(buf)
+ while pkt is not None:
+ if pkt is None:
+ break
+ queue.put(pkt)
+ buf = buf[len(pkt):]
+ pkt = extract_one_packet(buf)
+
+
+class RxQueue(PacketVerifier):
+ """Receive queue object.
+
+ This object creates raw socket, reads packets from it and provides
+ function to access them.
+
+ :param interface_name: Which interface to bind to.
+ :type interface_name: string
+ """
+
+ def __init__(self, interface_name):
+ PacketVerifier.__init__(self, interface_name)
+
+ self._queue = Queue()
+ self._proc = Process(target=packet_reader, args=(interface_name,
+ self._queue))
+ self._proc.daemon = True
+ self._proc.start()
+ time.sleep(2)
+
+ def recv(self, timeout=3, ignore=None):
+ """Read next received packet.
+
+ Returns scapy's Ether() object created from next packet in the queue.
+ Queue is being filled in parallel in subprocess. If no packet
+ arrives in given timeout queue.Empty exception will be risen.
+
+ :param timeout: How many seconds to wait for next packet.
+ :type timeout: int
+
+ :return: Ether() initialized object from packet data.
+ :rtype: scapy.Ether
+ """
+
+ pkt = self._queue.get(True, timeout=timeout)
+
+ if ignore is not None:
+ for i, ig_pkt in enumerate(ignore):
+ # Auto pad all packets in ignore list
+ ignore[i] = auto_pad(ig_pkt)
+ for ig_pkt in ignore:
+ if ig_pkt == pkt:
+ # Found the packet in ignore list, get another one
+ # TODO: subtract timeout - time_spent in here
+ ignore.remove(ig_pkt)
+ return self.recv(timeout, ignore)
+
+ return Ether(pkt)
+
+
+class TxQueue(PacketVerifier):
+ """Transmission queue object.
+
+ This object is used to send packets over RAW socket on a interface.
+
+ :param interface_name: Which interface to send packets from.
+ :type interface_name: string
+ """
+ def __init__(self, interface_name):
+ PacketVerifier.__init__(self, interface_name)
+
+ def send(self, pkt):
+ """Send packet out of the bound interface.
+
+ :param pkt: Packet to send.
+ :type pkt: string or scapy Packet derivative.
+ """
+ if isinstance(pkt, Packet):
+ pkt = str(pkt)
+ pkt = auto_pad(pkt)
+ self._sock.send(pkt)
+
+
+class Interface(object):
+ def __init__(self, if_name):
+ self.if_name = if_name
+ self.sent_packets = []
+ self.txq = TxQueue(if_name)
+ self.rxq = RxQueue(if_name)
+
+ def send_pkt(self, pkt):
+ self.sent_packets.append(pkt)
+ self.txq.send(pkt)
+
+ def recv_pkt(self, timeout=3):
+ while True:
+ pkt = self.rxq.recv(timeout, self.sent_packets)
+ # TODO: FIX FOLLOWING: DO NOT SKIP RARP IN ALL TESTS!!!
+ if pkt.type != 32821: # Skip RARP packets
+ return pkt
+
+ def close(self):
+ self.rxq._proc.terminate()
+
+
+def create_gratuitous_arp_request(src_mac, src_ip):
+ """Creates scapy representation of gratuitous ARP request"""
+ return (Ether(src=src_mac, dst='ff:ff:ff:ff:ff:ff') /
+ ARP(psrc=src_ip, hwsrc=src_mac, pdst=src_ip))
+
+
+def auto_pad(packet):
+ """Pads zeroes at the end of the packet if the total len < 60 bytes."""
+ padded = str(packet)
+ if len(padded) < 60:
+ padded += ('\0' * (60 - len(padded)))
+ return padded
+
diff --git a/resources/libraries/python/SetupFramework.py b/resources/libraries/python/SetupFramework.py
new file mode 100644
index 0000000000..47c609fada
--- /dev/null
+++ b/resources/libraries/python/SetupFramework.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 shlex
+from subprocess import Popen, PIPE, call
+from multiprocessing import Pool
+from tempfile import NamedTemporaryFile
+from os.path import basename
+from robot.api import logger
+from robot.libraries.BuiltIn import BuiltIn
+from ssh import SSH
+from constants import Constants as con
+from topology import NodeType
+
+__all__ = ["SetupFramework"]
+
+
+def pack_framework_dir():
+ """Pack the testing WS into temp file, return its name."""
+
+ tmpfile = NamedTemporaryFile(suffix=".tgz", prefix="openvpp-testing-")
+ file_name = tmpfile.name
+ tmpfile.close()
+
+ proc = Popen(
+ shlex.split("tar --exclude-vcs -zcf {0} .".format(file_name)),
+ stdout=PIPE, stderr=PIPE)
+ (stdout, stderr) = proc.communicate()
+
+ logger.debug(stdout)
+ logger.debug(stderr)
+
+ return_code = proc.wait()
+ if 0 != return_code:
+ raise Exception("Could not pack testing framework.")
+
+ return file_name
+
+
+def copy_tarball_to_node(tarball, node):
+ logger.console('Copying tarball to {0}'.format(node['host']))
+ ssh = SSH()
+ ssh.connect(node)
+
+ ssh.scp(tarball, "/tmp/")
+
+
+def extract_tarball_at_node(tarball, node):
+ logger.console('Extracting tarball to {0} on {1}'.format(
+ con.REMOTE_FW_DIR, node['host']))
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = 'sudo rm -rf {1}; mkdir {1} ; tar -zxf {0} -C {1}; ' \
+ 'rm -f {0}'.format(tarball, con.REMOTE_FW_DIR)
+ (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=30)
+ if 0 != ret_code:
+ logger.error('Unpack error: {0}'.format(stderr))
+ raise Exception('Failed to unpack {0} at node {1}'.format(
+ tarball, node['host']))
+
+
+def create_env_directory_at_node(node):
+ """Create fresh virtualenv to a directory, install pip requirements."""
+ logger.console('Extracting virtualenv, installing requirements.txt '
+ 'on {0}'.format(node['host']))
+ ssh = SSH()
+ ssh.connect(node)
+ (ret_code, stdout, stderr) = ssh.exec_command(
+ 'cd {0} && rm -rf env && virtualenv env && '
+ '. env/bin/activate && '
+ 'pip install -r requirements.txt'.format(con.REMOTE_FW_DIR))
+ if 0 != ret_code:
+ logger.error('Virtualenv creation error: {0}'.format(stdout + stderr))
+ raise Exception('Virtualenv setup failed')
+
+
+def setup_node(args):
+ tarball, remote_tarball, node = args
+ copy_tarball_to_node(tarball, node)
+ extract_tarball_at_node(remote_tarball, node)
+ if node['type'] == NodeType.TG:
+ create_env_directory_at_node(node)
+
+
+def delete_local_tarball(tarball):
+ call(shlex.split('sh -c "rm {0} > /dev/null 2>&1"'.format(tarball)))
+
+
+class SetupFramework(object):
+ """Setup suite run on topology nodes.
+
+ Many VAT/CLI based tests need the scripts at remote hosts before executing
+ them. This class packs the whole testing directory and copies it over
+ to all nodes in topology under /tmp/
+ """
+
+ def __init__(self):
+ pass
+
+ def setup_framework(self, nodes):
+ """Pack the whole directory and extract in temp on each node."""
+
+ tarball = pack_framework_dir()
+ msg = 'Framework packed to {0}'.format(tarball)
+ logger.console(msg)
+ logger.trace(msg)
+ remote_tarball = "/tmp/{0}".format(basename(tarball))
+
+ # Turn off loggining since we use multiprocessing
+ log_level = BuiltIn().set_log_level('NONE')
+ params = ((tarball, remote_tarball, node) for node in nodes.values())
+ pool = Pool(processes=len(nodes))
+ result = pool.map_async(setup_node, params)
+ pool.close()
+ pool.join()
+
+ logger.info(
+ 'Executed node setups in parallel, waiting for processes to end')
+ result.wait()
+
+ logger.info('Results: {0}'.format(result.get()))
+
+ # Turn on loggining
+ BuiltIn().set_log_level(log_level)
+ logger.trace('Test framework copied to all topology nodes')
+ delete_local_tarball(tarball)
diff --git a/resources/libraries/python/TGSetup.py b/resources/libraries/python/TGSetup.py
new file mode 100644
index 0000000000..3e372e9464
--- /dev/null
+++ b/resources/libraries/python/TGSetup.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""TG Setup library."""
+
+from topology import NodeType
+from InterfaceSetup import InterfaceSetup
+
+
+class TGSetup(object):
+ """TG setup before test."""
+
+ @staticmethod
+ def all_tgs_set_interface_default_driver(nodes):
+ """Setup interfaces default driver for all TGs in given topology.
+
+ :param nodes: Nodes in topology.
+ :type nodes: dict
+ """
+ for node in nodes.values():
+ if node['type'] == NodeType.TG:
+ InterfaceSetup.tg_set_interfaces_default_driver(node)
diff --git a/resources/libraries/python/TrafficGenerator.py b/resources/libraries/python/TrafficGenerator.py
new file mode 100644
index 0000000000..d86917a181
--- /dev/null
+++ b/resources/libraries/python/TrafficGenerator.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 ssh import SSH
+from robot.api import logger
+
+__all__ = ['TrafficGenerator']
+
+class TrafficGenerator(object):
+
+ def __init__(self):
+ self._result = None
+ self._loss = None
+ self._sent = None
+ self._received = None
+
+
+ def send_traffic_on(self, node, tx_port, rx_port, duration, rate,
+ framesize):
+ ssh = SSH()
+ ssh.connect(node)
+
+ (ret, stdout, stderr) = ssh.exec_command(
+ "sh -c 'cd MoonGen && sudo -S build/MoonGen "
+ "rfc2544/benchmarks/vpp-frameloss.lua --txport 0 --rxport 1 "
+ "--duration {0} --rate {1} --framesize {2}'".format(
+ duration, rate, framesize),
+ timeout=int(duration)+60)
+
+ logger.trace(ret)
+ logger.trace(stdout)
+ logger.trace(stderr)
+
+ for line in stdout.splitlines():
+ pass
+
+ self._result = line
+ logger.info('TrafficGen result: {0}'.format(self._result))
+
+ self._loss = self._result.split(', ')[3].split('=')[1]
+
+ return self._result
+
+ def no_traffic_loss_occured(self):
+ if self._loss is None:
+ raise Exception('The traffic generation has not been issued')
+ if self._loss != '0':
+ raise Exception('Traffic loss occured: {0}'.format(self._loss))
diff --git a/resources/libraries/python/TrafficScriptArg.py b/resources/libraries/python/TrafficScriptArg.py
new file mode 100644
index 0000000000..ab76f29b8e
--- /dev/null
+++ b/resources/libraries/python/TrafficScriptArg.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic scripts argument parser library."""
+
+import argparse
+
+
+class TrafficScriptArg(object):
+ """Traffic scripts argument parser.
+
+ Parse arguments for traffic script. Default has two arguments '--tx_if'
+ and '--rx_if'. You can provide more arguments. All arguments have string
+ representation of the value.
+
+ :param more_args: List of aditional arguments (optional).
+ :type more_args: list
+
+ :Example:
+
+ >>> from TrafficScriptArg import TrafficScriptArg
+ >>> args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip'])
+ """
+
+ def __init__(self, more_args=None):
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--tx_if", help="interface that sends traffic")
+ parser.add_argument("--rx_if", help="interface that receives traffic")
+
+ if more_args is not None:
+ for arg in more_args:
+ arg_name = '--{0}'.format(arg)
+ parser.add_argument(arg_name)
+
+ self._parser = parser
+ self._args = vars(parser.parse_args())
+
+ def get_arg(self, arg_name):
+ """Get argument value.
+
+ :param arg_name: Argument name.
+ :type arg_name: str
+ :return: Argument value.
+ :rtype: str
+ """
+ arg_val = self._args.get(arg_name)
+ if arg_val is None:
+ raise Exception("Argument '{0}' not found".format(arg_name))
+
+ return arg_val
diff --git a/resources/libraries/python/TrafficScriptExecutor.py b/resources/libraries/python/TrafficScriptExecutor.py
new file mode 100644
index 0000000000..2e65a520d0
--- /dev/null
+++ b/resources/libraries/python/TrafficScriptExecutor.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script executor library."""
+
+from constants import Constants
+from ssh import SSH
+from robot.api import logger
+
+__all__ = ['TrafficScriptExecutor']
+
+
+class TrafficScriptExecutor(object):
+ """Traffic script executor utilities."""
+
+ @staticmethod
+ def _escape(string):
+ """Escape quotation mark and dollar mark for shell command.
+
+ :param string: String to escape.
+ :type string: str
+ :return: Escaped string.
+ :rtype: str
+ """
+ return string.replace('"', '\\"').replace("$", "\\$")
+
+ @staticmethod
+ def run_traffic_script_on_node(script_file_name, node, script_args,
+ timeout=10):
+ """Run traffic script on the TG node.
+
+ :param script_file_name: Traffic script name
+ :param node: Node to run traffic script on.
+ :param script_args: Traffic scripts arguments.
+ :param timeout: Timeout (optional).
+ :type script_file_name: str
+ :type node: dict
+ :type script_args: str
+ :type timeout: int
+ """
+ logger.trace("{}".format(timeout))
+ ssh = SSH()
+ ssh.connect(node)
+ cmd = ("cd {}; virtualenv env && " +
+ "export PYTHONPATH=${{PWD}}; " +
+ ". ${{PWD}}/env/bin/activate; " +
+ "resources/traffic_scripts/{} {}") \
+ .format(Constants.REMOTE_FW_DIR, script_file_name,
+ script_args)
+ (ret_code, stdout, stderr) = ssh.exec_command_sudo(
+ 'sh -c "{}"'.format(TrafficScriptExecutor._escape(cmd)),
+ timeout=timeout)
+ logger.debug("stdout: {}".format(stdout))
+ logger.debug("stderr: {}".format(stderr))
+ logger.debug("ret_code: {}".format(ret_code))
+ if ret_code != 0:
+ raise Exception("Traffic script execution failed")
+
+ @staticmethod
+ def traffic_script_gen_arg(rx_if, tx_if, src_mac, dst_mac, src_ip, dst_ip):
+ """Generate traffic script basic arguments string.
+
+ :param rx_if: Interface that sends traffic.
+ :param tx_if: Interface that receives traffic.
+ :param src_mac: Source MAC address.
+ :param dst_mac: Destination MAC address.
+ :param src_ip: Source IP address.
+ :param dst_ip: Destination IP address.
+ :type rx_if: str
+ :type tx_if: str
+ :type src_mac: str
+ :type dst_mac: str
+ :type src_ip: str
+ :type dst_ip: str
+ :return: Traffic script arguments string.
+ :rtype: str
+ """
+ args = '--rx_if {0} --tx_if {1} --src_mac {2} --dst_mac {3} --src_ip' \
+ ' {4} --dst_ip {5}'.format(rx_if, tx_if, src_mac, dst_mac, src_ip,
+ dst_ip)
+ return args
diff --git a/resources/libraries/python/VatConfigGenerator.py b/resources/libraries/python/VatConfigGenerator.py
new file mode 100644
index 0000000000..98be9d3448
--- /dev/null
+++ b/resources/libraries/python/VatConfigGenerator.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Can be used to generate VAT scripts from VAT template files."""
+
+from robot.api import logger
+
+
+class VatConfigGenerator(object):
+ """Generates VAT configuration scripts from VAT script template files.
+ """
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def generate_vat_config_file(template_file, env_var_dict, out_file):
+ """ Write VAT configuration script to out file.
+
+ Generates VAT configuration script from template using
+ dictionary containing environment variables
+ :param template_file: file that contains the VAT script template
+ :param env_var_dict: python dictionary that maps test
+ environment variables
+ """
+
+ template_data = open(template_file).read()
+ logger.trace("Loaded template file: \n '{0}'".format(template_data))
+ generated_config = template_data.format(**env_var_dict)
+ logger.trace("Generated script file: \n '{0}'".format(generated_config))
+ with open(out_file, 'w') as work_file:
+ work_file.write(generated_config)
+
+ @staticmethod
+ def generate_vat_config_string(template_file, env_var_dict):
+ """ Return wat config string generated from template.
+
+ Generates VAT configuration script from template using
+ dictionary containing environment variables
+ :param template_file: file that contains the VAT script template
+ :param env_var_dict: python dictionary that maps test
+ environment variables
+ """
+
+ template_data = open(template_file).read()
+ logger.trace("Loaded template file: \n '{0}'".format(template_data))
+ generated_config = template_data.format(**env_var_dict)
+ logger.trace("Generated script file: \n '{0}'".format(generated_config))
+ return generated_config
diff --git a/resources/libraries/python/VatExecutor.py b/resources/libraries/python/VatExecutor.py
new file mode 100644
index 0000000000..5582a869b7
--- /dev/null
+++ b/resources/libraries/python/VatExecutor.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 ssh import SSH
+from robot.api import logger
+from constants import Constants
+import json
+
+__all__ = ['VatExecutor']
+
+
+def cleanup_vat_json_output(json_output):
+ """Return VAT json output cleaned from VAT clutter.
+
+ Clean up VAT json output from clutter like vat# prompts and such
+ :param json_output: cluttered json output.
+ :return: cleaned up output json string
+ """
+
+ retval = json_output
+ clutter = ['vat#', 'dump_interface_table error: Misc']
+ for garbage in clutter:
+ retval = retval.replace(garbage, '')
+ return retval
+
+
+class VatExecutor(object):
+
+ def __init__(self):
+ self._stdout = None
+ self._stderr = None
+ self._ret_code = None
+
+ def execute_script(self, vat_name, node, timeout=10, json_out=True):
+ """Copy local_path script to node, execute it and return result.
+
+ :param vat_name: name of the vat script file. Only the file name of
+ the script is required, the resources path is prepended
+ automatically.
+ :param node: node to execute the VAT script on.
+ :param timeout: seconds to allow the script to run.
+ :param json_out: require json output.
+ :return: (rc, stdout, stderr) tuple.
+ """
+
+ ssh = SSH()
+ ssh.connect(node)
+
+ remote_file_path = '{0}/{1}/{2}'.format(Constants.REMOTE_FW_DIR,
+ Constants.RESOURCES_TPL_VAT,
+ vat_name)
+ # TODO this overwrites the output if the vat script has been used twice
+ remote_file_out = remote_file_path + ".out"
+
+ cmd = "sudo -S {vat} {json} < {input}".format(
+ vat=Constants.VAT_BIN_NAME,
+ json="json" if json_out is True else "",
+ input=remote_file_path)
+ (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout)
+ self._ret_code = ret_code
+ self._stdout = stdout
+ self._stderr = stderr
+
+ logger.trace("Command '{0}' returned {1}'".format(cmd, self._ret_code))
+ logger.trace("stdout: '{0}'".format(self._stdout))
+ logger.trace("stderr: '{0}'".format(self._stderr))
+
+ # TODO: download vpe_api_test output file
+ # self._delete_files(node, remote_file_path, remote_file_out)
+
+ def execute_script_json_out(self, vat_name, node, timeout=10,):
+ self.execute_script(vat_name, node, timeout, json_out=True)
+ self._stdout = cleanup_vat_json_output(self._stdout)
+
+ def _delete_files(self, node, *files):
+ ssh = SSH()
+ ssh.connect(node)
+ files = " ".join([str(x) for x in files])
+ ssh.exec_command("rm {0}".format(files))
+
+ def script_should_have_failed(self):
+ if self._ret_code is None:
+ raise Exception("First execute the script!")
+ if self._ret_code == 0:
+ raise AssertionError(
+ "Script execution passed, but failure was expected")
+
+ def script_should_have_passed(self):
+ if self._ret_code is None:
+ raise Exception("First execute the script!")
+ if self._ret_code != 0:
+ raise AssertionError(
+ "Script execution failed, but success was expected")
+
+ def get_script_stdout(self):
+ return self._stdout
+
+ def get_script_stderr(self):
+ return self._stderr
+
+ @staticmethod
+ def cmd_from_template(node, vat_template_file, **vat_args):
+ """Execute VAT script on specified node. This method supports
+ script templates with parameters
+ :param node: node in topology on witch the scrtipt is executed
+ :param vat_template_file: template file of VAT script
+ :param vat_args: arguments to the template file
+ :return: list of json objects returned by VAT
+ """
+ vat = VatTerminal(node)
+ ret = vat.vat_terminal_exec_cmd_from_template(vat_template_file,
+ **vat_args)
+ vat.vat_terminal_close()
+ return ret
+
+ @staticmethod
+ def copy_config_to_remote(node, local_path, remote_path):
+ # TODO: will be removed once v4 is merged to master.
+ """Copies vat configuration file to node
+
+ :param node: Remote node on which to copy the VAT configuration file
+ :param local_path: path of the VAT script on local device that launches
+ test cases.
+ :param remote_path: path on remote node where to copy the VAT
+ configuration script file
+ """
+ ssh = SSH()
+ ssh.connect(node)
+ logger.trace("Removing old file {}".format(remote_path))
+ ssh.exec_command_sudo("rm -f {}".format(remote_path))
+ ssh.scp(local_path, remote_path)
+
+
+class VatTerminal(object):
+ """VAT interactive terminal
+
+ :param node: Node to open VAT terminal on.
+ """
+
+ __VAT_PROMPT = "vat# "
+ __LINUX_PROMPT = ":~$ "
+
+ def __init__(self, node):
+ self._ssh = SSH()
+ self._ssh.connect(node)
+ self._tty = self._ssh.interactive_terminal_open()
+ self._ssh.interactive_terminal_exec_command(
+ self._tty,
+ 'sudo -S {vat} json'.format(vat=Constants.VAT_BIN_NAME),
+ self.__VAT_PROMPT)
+
+ def vat_terminal_exec_cmd(self, cmd):
+ """Execute command on the opened VAT terminal.
+
+ :param cmd: Command to be executed.
+
+ :return: Command output in python representation of JSON format.
+ """
+ logger.debug("Executing command in VAT terminal: {}".format(cmd));
+ out = self._ssh.interactive_terminal_exec_command(self._tty,
+ cmd,
+ self.__VAT_PROMPT)
+ logger.debug("VAT output: {}".format(out));
+ json_out = json.loads(out)
+ return json_out
+
+ def vat_terminal_close(self):
+ """Close VAT terminal."""
+ self._ssh.interactive_terminal_exec_command(self._tty,
+ 'quit',
+ self.__LINUX_PROMPT)
+ self._ssh.interactive_terminal_close(self._tty)
+
+ def vat_terminal_exec_cmd_from_template(self, vat_template_file, **args):
+ """Execute VAT script from a file.
+ :param vat_template_file: template file name of a VAT script
+ :param args: dictionary of parameters for VAT script
+ :return: list of json objects returned by VAT
+ """
+ file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT,
+ vat_template_file)
+ with open(file_path, 'r') as template_file:
+ cmd_template = template_file.readlines()
+ ret = []
+ for line_tmpl in cmd_template:
+ vat_cmd = line_tmpl.format(**args)
+ ret.append(self.vat_terminal_exec_cmd(vat_cmd))
+ return ret
diff --git a/resources/libraries/python/VppCounters.py b/resources/libraries/python/VppCounters.py
new file mode 100644
index 0000000000..f34d7a76d1
--- /dev/null
+++ b/resources/libraries/python/VppCounters.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""VPP counters utilities library."""
+
+import time
+from topology import NodeType, Topology
+from VatExecutor import VatExecutor, VatTerminal
+from robot.api import logger
+
+
+class VppCounters(object):
+ """VPP counters utilities."""
+
+ def __init__(self):
+ self._stats_table = None
+
+ def vpp_nodes_clear_interface_counters(self, nodes):
+ """Clear interface counters on all VPP nodes in topology.
+
+ :param nodes: Nodes in topology.
+ :type nodes: dict
+ """
+ for node in nodes.values():
+ if node['type'] == NodeType.DUT:
+ self.vpp_clear_interface_counters(node)
+
+ @staticmethod
+ def vpp_clear_interface_counters(node):
+ """Clear interface counters on VPP node.
+
+ :param node: Node to clear interface counters on.
+ :type node: dict
+ """
+ vat = VatExecutor()
+ vat.execute_script('clear_interface.vat', node)
+ vat.script_should_have_passed()
+
+ def vpp_dump_stats_table(self, node):
+ """Dump stats table on VPP node.
+
+ :param node: Node to dump stats table on.
+ :type node: dict
+ :return: Stats table.
+ """
+ vat = VatTerminal(node)
+ vat.vat_terminal_exec_cmd('want_stats enable')
+ for _ in range(0, 12):
+ stats_table = vat.vat_terminal_exec_cmd('dump_stats_table')
+ if_counters = stats_table['interface_counters']
+ if len(if_counters) > 0:
+ self._stats_table = stats_table
+ vat.vat_terminal_close()
+ return stats_table
+ time.sleep(1)
+
+ vat.vat_terminal_close()
+ return None
+
+ def vpp_get_ipv4_interface_counter(self, node, interface):
+ return self.vpp_get_ipv46_interface_counter(node, interface, False)
+
+ def vpp_get_ipv6_interface_counter(self, node, interface):
+ return self.vpp_get_ipv46_interface_counter(node, interface, True)
+
+ def vpp_get_ipv46_interface_counter(self, node, interface, is_ipv6=True):
+ """Return interface IPv4/IPv6 counter
+
+ :param node: Node to get interface IPv4/IPv6 counter on.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Interface IPv4/IPv6 counter.
+ :param is_ipv6: specify IP version
+ :type is_ipv6: bool
+ :rtype: int
+ """
+ version = 'ip6' if is_ipv6 else 'ip4'
+ topo = Topology()
+ if_index = topo.get_interface_sw_index(node, interface)
+ if if_index is None:
+ logger.trace('{i} sw_index not found.'.format(i=interface))
+ return 0
+
+ if_counters = self._stats_table.get('interface_counters')
+ if if_counters is None or len(if_counters) == 0:
+ logger.trace('No interface counters.')
+ return 0
+ for counter in if_counters:
+ if counter['vnet_counter_type'] == version:
+ data = counter['data']
+ return data[if_index]
+ logger.trace('{i} {v} counter not found.'.format(i=interface,
+ v=version))
+ return 0
diff --git a/resources/libraries/python/__init__.py b/resources/libraries/python/__init__.py
new file mode 100644
index 0000000000..16058f3941
--- /dev/null
+++ b/resources/libraries/python/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""
+__init__ file for directory resources/libraries/python
+"""
diff --git a/resources/libraries/python/constants.py b/resources/libraries/python/constants.py
new file mode 100644
index 0000000000..d7134cedcb
--- /dev/null
+++ b/resources/libraries/python/constants.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 Constants(object):
+ #OpenVPP testing directory location at topology nodes
+ REMOTE_FW_DIR = '/tmp/openvpp-testing'
+ RESOURCES_LIB_SH = 'resources/libraries/bash'
+ RESOURCES_TPL_VAT = 'resources/templates/vat'
+ #OpenVPP VAT binary name
+ VAT_BIN_NAME = 'vpe_api_test'
diff --git a/resources/libraries/python/parsers/JsonParser.py b/resources/libraries/python/parsers/JsonParser.py
new file mode 100644
index 0000000000..1d177670ff
--- /dev/null
+++ b/resources/libraries/python/parsers/JsonParser.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Used to parse Json files or Json data strings to dictionaries"""
+
+import json
+
+
+class JsonParser(object):
+ """Parses Json data string or files containing Json data strings"""
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def parse_data(json_data):
+ """Return list parsed from json data string.
+
+ Translates json data into list of values/dictionaries/lists
+ :param json_data: data in json format
+ :return: json data parsed as python list
+ """
+ parsed_data = json.loads(json_data)
+ return parsed_data
+
+ def parse_file(self, json_file):
+ """Return list parsed from file containing json string.
+
+ Translates json data found in file into list of
+ values/dictionaries/lists
+ :param json_file: file with json type data
+ :return: json data parsed as python list
+ """
+ input_data = open(json_file).read()
+ parsed_data = self.parse_data(input_data)
+ return parsed_data
diff --git a/resources/libraries/python/parsers/__init__.py b/resources/libraries/python/parsers/__init__.py
new file mode 100644
index 0000000000..5a0e0e1c5e
--- /dev/null
+++ b/resources/libraries/python/parsers/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
diff --git a/resources/libraries/python/ssh.py b/resources/libraries/python/ssh.py
new file mode 100644
index 0000000000..72e41c76a6
--- /dev/null
+++ b/resources/libraries/python/ssh.py
@@ -0,0 +1,235 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 paramiko
+from scp import SCPClient
+from time import time
+from robot.api import logger
+from interruptingcow import timeout
+from robot.utils.asserts import assert_equal, assert_not_equal
+
+__all__ = ["exec_cmd", "exec_cmd_no_error"]
+
+# TODO: load priv key
+
+
+class SSH(object):
+
+ __MAX_RECV_BUF = 10*1024*1024
+ __existing_connections = {}
+
+ def __init__(self):
+ self._ssh = paramiko.SSHClient()
+ self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self._hostname = None
+
+ def _node_hash(self, node):
+ return hash(frozenset([node['host'], node['port']]))
+
+ def connect(self, node):
+ """Connect to node prior to running exec_command or scp.
+
+ If there already is a connection to the node, this method reuses it.
+ """
+ self._hostname = node['host']
+ node_hash = self._node_hash(node)
+ if node_hash in self.__existing_connections:
+ self._ssh = self.__existing_connections[node_hash]
+ else:
+ start = time()
+ self._ssh.connect(node['host'], username=node['username'],
+ password=node['password'])
+ self.__existing_connections[node_hash] = self._ssh
+ logger.trace('connect took {} seconds'.format(time() - start))
+
+ def exec_command(self, cmd, timeout=10):
+ """Execute SSH command on a new channel on the connected Node.
+
+ Returns (return_code, stdout, stderr).
+ """
+ logger.trace('exec_command on {0}: {1}'.format(self._hostname, cmd))
+ start = time()
+ chan = self._ssh.get_transport().open_session()
+ if timeout is not None:
+ chan.settimeout(int(timeout))
+ chan.exec_command(cmd)
+ end = time()
+ logger.trace('exec_command "{0}" on {1} took {2} seconds'.format(
+ cmd, self._hostname, end-start))
+
+ stdout = ""
+ while True:
+ buf = chan.recv(self.__MAX_RECV_BUF)
+ stdout += buf
+ if not buf:
+ break
+
+ stderr = ""
+ while True:
+ buf = chan.recv_stderr(self.__MAX_RECV_BUF)
+ stderr += buf
+ if not buf:
+ break
+
+ return_code = chan.recv_exit_status()
+ logger.trace('chan_recv/_stderr took {} seconds'.format(time()-end))
+
+ return (return_code, stdout, stderr)
+
+ def exec_command_sudo(self, cmd, cmd_input=None, timeout=10):
+ """Execute SSH command with sudo on a new channel on the connected Node.
+
+ :param cmd: Command to be executed.
+ :param cmd_input: Input redirected to the command.
+ :param timeout: Timeout.
+ :return: return_code, stdout, stderr
+
+ :Example:
+
+ >>> from ssh import SSH
+ >>> ssh = SSH()
+ >>> ssh.connect(node)
+ >>> #Execute command without input (sudo -S cmd)
+ >>> ssh.exex_command_sudo("ifconfig eth0 down")
+ >>> #Execute command with input (sudo -S cmd <<< "input")
+ >>> ssh.exex_command_sudo("vpe_api_test", "dump_interface_table")
+ """
+ if cmd_input is None:
+ command = 'sudo -S {c}'.format(c=cmd)
+ else:
+ command = 'sudo -S {c} <<< "{i}"'.format(c=cmd, i=cmd_input)
+ return self.exec_command(command, timeout)
+
+ def interactive_terminal_open(self, time_out=10):
+ """Open interactive terminal on a new channel on the connected Node.
+
+ :param time_out: Timeout in seconds.
+ :return: SSH channel with opened terminal.
+
+ .. warning:: Interruptingcow is used here, and it uses
+ signal(SIGALRM) to let the operating system interrupt program
+ execution. This has the following limitations: Python signal
+ handlers only apply to the main thread, so you cannot use this
+ from other threads. You must not use this in a program that
+ uses SIGALRM itself (this includes certain profilers)
+ """
+ chan = self._ssh.get_transport().open_session()
+ chan.get_pty()
+ chan.invoke_shell()
+ chan.settimeout(int(time_out))
+
+ buf = ''
+ try:
+ with timeout(time_out, exception=RuntimeError):
+ while not buf.endswith(':~$ '):
+ if chan.recv_ready():
+ buf = chan.recv(4096)
+ except RuntimeError:
+ raise Exception('Open interactive terminal timeout.')
+ return chan
+
+ def interactive_terminal_exec_command(self, chan, cmd, prompt,
+ time_out=10):
+ """Execute command on interactive terminal.
+
+ interactive_terminal_open() method has to be called first!
+
+ :param chan: SSH channel with opened terminal.
+ :param cmd: Command to be executed.
+ :param prompt: Command prompt, sequence of characters used to
+ indicate readiness to accept commands.
+ :param time_out: Timeout in seconds.
+ :return: Command output.
+
+ .. warning:: Interruptingcow is used here, and it uses
+ signal(SIGALRM) to let the operating system interrupt program
+ execution. This has the following limitations: Python signal
+ handlers only apply to the main thread, so you cannot use this
+ from other threads. You must not use this in a program that
+ uses SIGALRM itself (this includes certain profilers)
+ """
+ chan.sendall('{c}\n'.format(c=cmd))
+ buf = ''
+ try:
+ with timeout(time_out, exception=RuntimeError):
+ while not buf.endswith(prompt):
+ if chan.recv_ready():
+ buf += chan.recv(4096)
+ except RuntimeError:
+ raise Exception("Exec '{c}' timeout.".format(c=cmd))
+ tmp = buf.replace(cmd.replace('\n', ''), '')
+ return tmp.replace(prompt, '')
+
+ def interactive_terminal_close(self, chan):
+ """Close interactive terminal SSH channel.
+
+ :param: chan: SSH channel to be closed.
+ """
+ chan.close()
+
+ def scp(self, local_path, remote_path):
+ """Copy files from local_path to remote_path.
+
+ connect() method has to be called first!
+ """
+ logger.trace('SCP {0} to {1}:{2}'.format(
+ local_path, self._hostname, remote_path))
+ # SCPCLient takes a paramiko transport as its only argument
+ scp = SCPClient(self._ssh.get_transport())
+ start = time()
+ scp.put(local_path, remote_path)
+ scp.close()
+ end = time()
+ logger.trace('SCP took {0} seconds'.format(end-start))
+
+
+def exec_cmd(node, cmd, timeout=None, sudo=False):
+ """Convenience function to ssh/exec/return rc, out & err.
+
+ Returns (rc, stdout, stderr).
+ """
+ if node is None:
+ raise TypeError('Node parameter is None')
+ if cmd is None:
+ raise TypeError('Command parameter is None')
+ if len(cmd) == 0:
+ raise ValueError('Empty command parameter')
+
+ ssh = SSH()
+ try:
+ ssh.connect(node)
+ except Exception, e:
+ logger.error("Failed to connect to node" + e)
+ return None
+
+ try:
+ if not sudo:
+ (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=timeout)
+ else:
+ (ret_code, stdout, stderr) = ssh.exec_command_sudo(cmd,
+ timeout=timeout)
+ except Exception, e:
+ logger.error(e)
+ return None
+
+ return (ret_code, stdout, stderr)
+
+def exec_cmd_no_error(node, cmd, timeout=None, sudo=False):
+ """Convenience function to ssh/exec/return out & err.
+ Verifies that return code is zero.
+
+ Returns (stdout, stderr).
+ """
+ (rc, stdout, stderr) = exec_cmd(node,cmd, timeout=timeout, sudo=sudo)
+ assert_equal(rc, 0, 'Command execution failed: "{}"\n{}'.
+ format(cmd, stderr))
+ return (stdout, stderr)
diff --git a/resources/libraries/python/topology.py b/resources/libraries/python/topology.py
new file mode 100644
index 0000000000..522de37d13
--- /dev/null
+++ b/resources/libraries/python/topology.py
@@ -0,0 +1,539 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Defines nodes and topology structure."""
+
+from resources.libraries.python.parsers.JsonParser import JsonParser
+from resources.libraries.python.VatExecutor import VatExecutor
+from resources.libraries.python.ssh import SSH
+from resources.libraries.python.InterfaceSetup import InterfaceSetup
+from robot.api import logger
+from robot.libraries.BuiltIn import BuiltIn
+from robot.api.deco import keyword
+from yaml import load
+
+__all__ = ["DICT__nodes", 'Topology']
+
+
+def load_topo_from_yaml():
+ """Loads topology from file defined in "${TOPOLOGY_PATH}" variable
+
+ :return: nodes from loaded topology
+ """
+ topo_path = BuiltIn().get_variable_value("${TOPOLOGY_PATH}")
+
+ with open(topo_path) as work_file:
+ return load(work_file.read())['nodes']
+
+
+class NodeType(object):
+ """Defines node types used in topology dictionaries"""
+ # Device Under Test (this node has VPP running on it)
+ DUT = 'DUT'
+ # Traffic Generator (this node has traffic generator on it)
+ TG = 'TG'
+
+DICT__nodes = load_topo_from_yaml()
+
+
+class Topology(object):
+ """Topology data manipulation and extraction methods
+
+ Defines methods used for manipulation and extraction of data from
+ the used topology.
+ """
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def get_node_by_hostname(nodes, hostname):
+ """Get node from nodes of the topology by hostname.
+
+ :param nodes: Nodes of the test topology.
+ :param hostname: Host name.
+ :type nodes: dict
+ :type hostname: str
+ :return: Node dictionary or None if not found.
+ """
+ for node in nodes.values():
+ if node['host'] == hostname:
+ return node
+
+ return None
+
+ @staticmethod
+ def get_links(nodes):
+ """Get list of links(networks) in the topology.
+
+ :param nodes: Nodes of the test topology.
+ :type nodes: dict
+ :return: Links in the topology.
+ :rtype: list
+ """
+ links = []
+
+ for node in nodes.values():
+ for interface in node['interfaces'].values():
+ link = interface.get('link')
+ if link is not None:
+ if link not in links:
+ links.append(link)
+
+ return links
+
+ @staticmethod
+ def _get_interface_by_key_value(node, key, value):
+ """ Return node interface name according to key and value
+
+ :param node: :param node: the node dictionary
+ :param key: key by which to select the interface.
+ :param value: value that should be found using the key.
+ :return:
+ """
+
+ interfaces = node['interfaces']
+ retval = None
+ for interface in interfaces.values():
+ k_val = interface.get(key)
+ if k_val is not None:
+ if k_val == value:
+ retval = interface['name']
+ break
+ return retval
+
+ def get_interface_by_link_name(self, node, link_name):
+ """Return interface name of link on node.
+
+ This method returns the interface name asociated with a given link
+ for a given node.
+ :param link_name: name of the link that a interface is connected to.
+ :param node: the node topology dictionary
+ :return: interface name of the interface connected to the given link
+ """
+
+ return self._get_interface_by_key_value(node, "link", link_name)
+
+ def get_interfaces_by_link_names(self, node, link_names):
+ """Return dictionary of dicitonaries {"interfaceN", interface name}.
+
+ This method returns the interface names asociated with given links
+ for a given node.
+ The resulting dictionary can be then used to with VatConfigGenerator
+ to generate a VAT script with proper interface names.
+ :param link_names: list of names of the link that a interface is
+ connected to.
+ :param node: the node topology directory
+ :return: dictionary of interface names that are connected to the given
+ links
+ """
+
+ retval = {}
+ interface_key_tpl = "interface{}"
+ interface_number = 1
+ for link_name in link_names:
+ interface_name = self.get_interface_by_link_name(node, link_name)
+ interface_key = interface_key_tpl.format(str(interface_number))
+ retval[interface_key] = interface_name
+ interface_number += 1
+ return retval
+
+ def get_interface_by_sw_index(self, node, sw_index):
+ """Return interface name of link on node.
+
+ This method returns the interface name asociated with a software index
+ assigned to the interface by vpp for a given node.
+ :param sw_index: sw_index of the link that a interface is connected to.
+ :param node: the node topology dictionary
+ :return: interface name of the interface connected to the given link
+ """
+
+ return self._get_interface_by_key_value(node, "vpp_sw_index", sw_index)
+
+ @staticmethod
+ def convert_mac_to_number_list(mac_address):
+ """Convert mac address string to list of decimal numbers.
+
+ Converts a : separated mac address to decimal number list as used
+ in json interface dump.
+ :param mac_address: string mac address
+ :return: list representation of mac address
+ """
+
+ list_mac = []
+ for num in mac_address.split(":"):
+ list_mac.append(int(num, 16))
+ return list_mac
+
+ def _extract_vpp_interface_by_mac(self, interfaces_list, mac_address):
+ """Returns interface dictionary from interface_list by mac address.
+
+ Extracts interface dictionary from all of the interfaces in interfaces
+ list parsed from json according to mac_address of the interface
+ :param interfaces_list: dictionary of all interfaces parsed from json
+ :param mac_address: string mac address of interface we are looking for
+ :return: interface dictionary from json
+ """
+
+ interface_dict = {}
+ list_mac_address = self.convert_mac_to_number_list(mac_address)
+ logger.trace(list_mac_address.__str__())
+ for interface in interfaces_list:
+ # TODO: create vat json integrity checking and move there
+ if "l2_address" not in interface:
+ raise KeyError(
+ "key l2_address not found in interface dict."
+ "Probably input list is not parsed from correct VAT "
+ "json output.")
+ if "l2_address_length" not in interface:
+ raise KeyError(
+ "key l2_address_length not found in interface "
+ "dict. Probably input list is not parsed from correct "
+ "VAT json output.")
+ mac_from_json = interface["l2_address"][:6]
+ if mac_from_json == list_mac_address:
+ if interface["l2_address_length"] != 6:
+ raise ValueError("l2_address_length value is not 6.")
+ interface_dict = interface
+ break
+ return interface_dict
+
+ def vpp_interface_name_from_json_by_mac(self, json_data, mac_address):
+ """Return vpp interface name string from VAT interface dump json output
+
+ Extracts the name given to an interface by VPP.
+ These interface names differ from what you would see if you
+ used the ipconfig or similar command.
+ Required json data can be obtained by calling :
+ VatExecutor.execute_script_json_out("dump_interfaces.vat", node)
+ :param json_data: string json data from sw_interface_dump VAT command
+ :param mac_address: string containing mac address of interface
+ whose vpp name we wish to discover.
+ :return: string vpp interface name
+ """
+
+ interfaces_list = JsonParser().parse_data(json_data)
+ # TODO: checking if json data is parsed correctly
+ interface_dict = self._extract_vpp_interface_by_mac(interfaces_list,
+ mac_address)
+ interface_name = interface_dict["interface_name"]
+ return interface_name
+
+ def _update_node_interface_data_from_json(self, node, interface_dump_json):
+ """ Update node vpp data in node__DICT from json interface dump.
+
+ This method updates vpp interface names and sw indexexs according to
+ interface mac addresses found in interface_dump_json
+ :param node: node dictionary
+ :param interface_dump_json: json output from dump_interface_list VAT
+ command
+ """
+
+ interface_list = JsonParser().parse_data(interface_dump_json)
+ for ifc in node['interfaces'].values():
+ if 'link' not in ifc:
+ continue
+ if_mac = ifc['mac_address']
+ interface_dict = self._extract_vpp_interface_by_mac(interface_list,
+ if_mac)
+ ifc['name'] = interface_dict["interface_name"]
+ ifc['vpp_sw_index'] = interface_dict["sw_if_index"]
+
+ def update_vpp_interface_data_on_node(self, node):
+ """Update vpp generated interface data for a given node in DICT__nodes
+
+ Updates interface names, software index numbers and any other details
+ generated specifically by vpp that are unknown before testcase run.
+ :param node: Node selected from DICT__nodes
+ """
+
+ vat_executor = VatExecutor()
+ vat_executor.execute_script_json_out("dump_interfaces.vat", node)
+ interface_dump_json = vat_executor.get_script_stdout()
+ self._update_node_interface_data_from_json(node,
+ interface_dump_json)
+
+ @staticmethod
+ def update_tg_interface_data_on_node(node):
+ """Update interface name for TG/linux node in DICT__nodes
+
+ :param node: Node selected from DICT__nodes.
+ :type node: dict
+
+ .. note::
+ # for dev in `ls /sys/class/net/`;
+ > do echo "\"`cat /sys/class/net/$dev/address`\": \"$dev\""; done
+ "52:54:00:9f:82:63": "eth0"
+ "52:54:00:77:ae:a9": "eth1"
+ "52:54:00:e1:8a:0f": "eth2"
+ "00:00:00:00:00:00": "lo"
+
+ .. todo:: parse lshw -json instead
+ """
+ # First setup interface driver specified in yaml file
+ InterfaceSetup.tg_set_interfaces_default_driver(node)
+
+ # Get interface names
+ ssh = SSH()
+ ssh.connect(node)
+
+ cmd = 'for dev in `ls /sys/class/net/`; do echo "\\"`cat ' \
+ '/sys/class/net/$dev/address`\\": \\"$dev\\""; done;'
+
+ (ret_code, stdout, _) = ssh.exec_command(cmd)
+ if int(ret_code) != 0:
+ raise Exception('Get interface name and MAC failed')
+ tmp = "{" + stdout.rstrip().replace('\n', ',') + "}"
+ interfaces = JsonParser().parse_data(tmp)
+ for if_k, if_v in node['interfaces'].items():
+ if if_k == 'mgmt':
+ continue
+ name = interfaces.get(if_v['mac_address'])
+ if name is None:
+ continue
+ if_v['name'] = name
+
+ # Set udev rules for interfaces
+ InterfaceSetup.tg_set_interfaces_udev_rules(node)
+
+ def update_all_interface_data_on_all_nodes(self, nodes):
+ """ Update interface names on all nodes in DICT__nodes
+
+ :param nodes: Nodes in the topology.
+ :type nodes: dict
+
+ This method updates the topology dictionary by querying interface lists
+ of all nodes mentioned in the topology dictionary.
+ It does this by dumping interface list to json output from all devices
+ using vpe_api_test, and pairing known information from topology
+ (mac address/pci address of interface) to state from VPP.
+ For TG/linux nodes add interface name only.
+ """
+
+ for node_data in nodes.values():
+ if node_data['type'] == NodeType.DUT:
+ self.update_vpp_interface_data_on_node(node_data)
+ elif node_data['type'] == NodeType.TG:
+ self.update_tg_interface_data_on_node(node_data)
+
+ @staticmethod
+ def get_interface_sw_index(node, interface):
+ """Get VPP sw_index for the interface.
+
+ :param node: Node to get interface sw_index on.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Return sw_index or None if not found.
+ """
+ for port in node['interfaces'].values():
+ port_name = port.get('name')
+ if port_name is None:
+ continue
+ if port_name == interface:
+ return port.get('vpp_sw_index')
+
+ return None
+
+ @staticmethod
+ def get_interface_mac(node, interface):
+ """Get MAC address for the interface.
+
+ :param node: Node to get interface sw_index on.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Return MAC or None if not found.
+ """
+ for port in node['interfaces'].values():
+ port_name = port.get('name')
+ if port_name is None:
+ continue
+ if port_name == interface:
+ return port.get('mac_address')
+
+ return None
+
+ @staticmethod
+ def get_adjacent_interface(node, interface_name):
+ """Get interface adjacent to specified interface on local network.
+
+ :param node: Node that contains specified interface.
+ :param interface_name: Interface name.
+ :type node: dict
+ :type interface_name: str
+ :return: Return interface or None if not found.
+ :rtype: dict
+ """
+ link_name = None
+ # get link name where the interface belongs to
+ for _, port_data in node['interfaces'].iteritems():
+ if port_data['name'] == interface_name:
+ link_name = port_data['link']
+ break
+
+ if link_name is None:
+ return None
+
+ # find link
+ for _, node_data in DICT__nodes.iteritems():
+ # skip self
+ if node_data['host'] == node['host']:
+ continue
+ for interface, interface_data \
+ in node_data['interfaces'].iteritems():
+ if 'link' not in interface_data:
+ continue
+ if interface_data['link'] == link_name:
+ return node_data['interfaces'][interface]
+
+ @staticmethod
+ def get_interface_pci_addr(node, interface):
+ """Get interface PCI address.
+
+ :param node: Node to get interface PCI address on.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Return PCI address or None if not found.
+ """
+ for port in node['interfaces'].values():
+ if interface == port.get('name'):
+ return port.get('pci_address')
+ return None
+
+ @staticmethod
+ def get_interface_driver(node, interface):
+ """Get interface driver.
+
+ :param node: Node to get interface driver on.
+ :param interface: Interface name.
+ :type node: dict
+ :type interface: str
+ :return: Return interface driver or None if not found.
+ """
+ for port in node['interfaces'].values():
+ if interface == port.get('name'):
+ return port.get('driver')
+ return None
+
+ @staticmethod
+ def get_node_link_mac(node, link_name):
+ """Return interface mac address by link name
+
+ :param node: Node to get interface sw_index on
+ :param link_name: link name
+ :type node: dict
+ :type link_name: string
+ :return: mac address string
+ """
+ for port in node['interfaces'].values():
+ if port.get('link') == link_name:
+ return port.get('mac_address')
+ return None
+
+ @staticmethod
+ def _get_node_active_link_names(node):
+ """Returns list of link names that are other than mgmt links
+
+ :param node: node topology dictionary
+ :return: list of strings that represent link names occupied by the node
+ """
+ interfaces = node['interfaces']
+ link_names = []
+ for interface in interfaces.values():
+ if 'link' in interface:
+ link_names.append(interface['link'])
+ if len(link_names) == 0:
+ link_names = None
+ return link_names
+
+ @keyword('Get active links connecting "${node1}" and "${node2}"')
+ def get_active_connecting_links(self, node1, node2):
+ """Returns list of link names that connect together node1 and node2
+
+ :param node1: node topology dictionary
+ :param node2: node topology dictionary
+ :return: list of strings that represent connecting link names
+ """
+
+ logger.trace("node1: {}".format(str(node1)))
+ logger.trace("node2: {}".format(str(node2)))
+ node1_links = self._get_node_active_link_names(node1)
+ node2_links = self._get_node_active_link_names(node2)
+ connecting_links = list(set(node1_links).intersection(node2_links))
+
+ return connecting_links
+
+ @keyword('Get first active connecting link between node "${node1}" and '
+ '"${node2}"')
+ def get_first_active_connecting_link(self, node1, node2):
+ """
+
+ :param node1: Connected node
+ :type node1: dict
+ :param node2: Connected node
+ :type node2: dict
+ :return: name of link connecting the two nodes together
+ :raises: RuntimeError
+ """
+
+ connecting_links = self.get_active_connecting_links(node1, node2)
+ if len(connecting_links) == 0:
+ raise RuntimeError("No links connecting the nodes were found")
+ else:
+ return connecting_links[0]
+
+ @keyword('Get egress interfaces on "${node1}" for link with "${node2}"')
+ def get_egress_interfaces_for_nodes(self, node1, node2):
+ """Get egress interfaces on node1 for link with node2.
+
+ :param node1: First node, node to get egress interface on.
+ :param node2: Second node.
+ :type node1: dict
+ :type node2: dict
+ :return: Engress interfaces.
+ :rtype: list
+ """
+ interfaces = []
+ links = self.get_active_connecting_links(node1, node2)
+ if len(links) == 0:
+ raise RuntimeError('No link between nodes')
+ for interface in node1['interfaces'].values():
+ link = interface.get('link')
+ if link is None:
+ continue
+ if link in links:
+ continue
+ name = interface.get('name')
+ if name is None:
+ continue
+ interfaces.append(name)
+ return interfaces
+
+ @keyword('Get first egress interface on "${node1}" for link with '
+ '"${node2}"')
+ def get_first_egress_interface_for_nodes(self, node1, node2):
+ """Get first egress interface on node1 for link with node2.
+
+ :param node1: First node, node to get egress interface on.
+ :param node2: Second node.
+ :type node1: dict
+ :type node2: dict
+ :return: Engress interface.
+ :rtype: str
+ """
+ interfaces = self.get_egress_interfaces_for_nodes(node1, node2)
+ if not interfaces:
+ raise RuntimeError('No engress interface for nodes')
+ return interfaces[0]
diff --git a/resources/libraries/robot/bridge_domain.robot b/resources/libraries/robot/bridge_domain.robot
new file mode 100644
index 0000000000..fc3705700e
--- /dev/null
+++ b/resources/libraries/robot/bridge_domain.robot
@@ -0,0 +1,54 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Library | resources/libraries/python/VatExecutor.py
+| Library | resources/libraries/python/VatConfigGenerator.py
+| Library | resources.libraries.python.topology.Topology
+| Library | resources/libraries/python/TrafficScriptExecutor.py
+| Variables | resources/libraries/python/constants.py
+
+*** Variables ***
+| ${VAT_BD_TEMPLATE} | ${Constants.RESOURCES_TPL_VAT}/l2_bridge_domain.vat
+| ${VAT_BD_GEN_FILE} | ${Constants.RESOURCES_TPL_VAT}/l2_bridge_domain_gen.vat
+| ${VAT_BD_REMOTE_PATH} | ${Constants.REMOTE_FW_DIR}/l2_bridge_domain_gen.vat
+
+*** Keywords ***
+| Setup l2 bridge on node "${node}" via links "${link_names}"
+| | ${interface_config}= | Get Interfaces By Link Names | ${node} | ${link_names}
+| | ${commands}= | Generate Vat Config File | ${VAT_BD_TEMPLATE} | ${interface_config} | ${VAT_BD_GEN_FILE}
+| | Copy Config To Remote | ${node} | ${VAT_BD_GEN_FILE} | ${VAT_BD_REMOTE_PATH}
+# TODO: will be removed once v4 is merged to master.
+| | Execute Script | l2_bridge_domain_gen.vat | ${node} | json_out=False
+| | Script Should Have Passed
+
+| Send traffic on node "${node}" from link "${link1}" to link "${link2}"
+| | ${src_port}= | Get Interface By Link Name | ${node} | ${link1}
+| | ${dst_port}= | Get Interface By Link Name | ${node} | ${link2}
+| | ${src_ip}= | Set Variable | 192.168.100.1
+| | ${dst_ip}= | Set Variable | 192.168.100.2
+| | ${src_mac}= | Get Node Link Mac | ${node} | ${link1}
+| | ${dst_mac}= | Get Node Link Mac | ${node} | ${link2}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac} | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | send_ip_icmp.py | ${node} | ${args}
+
+| Setup TG "${tg}" DUT1 "${dut1}" and DUT2 "${dut2}" for 3 node l2 bridge domain test
+| | ${DUT1_DUT2_link}= | Get first active connecting link between node "${dut1}" and "${dut2}"
+| | ${DUT1_TG_link}= | Get first active connecting link between node "${dut1}" and "${tg}"
+| | ${DUT2_TG_link}= | Get first active connecting link between node "${dut2}" and "${tg}"
+| | ${tg_traffic_links}= | Create List | ${DUT1_TG_link} | ${DUT2_TG_link}
+| | ${DUT1_BD_links}= | Create_list | ${DUT1_DUT2_link} | ${DUT1_TG_link}
+| | ${DUT2_BD_links}= | Create_list | ${DUT1_DUT2_link} | ${DUT2_TG_link}
+| | Setup l2 bridge on node "${dut1}" via links "${DUT1_BD_links}"
+| | Setup l2 bridge on node "${dut2}" via links "${DUT2_BD_links}"
+| | [Return] | ${tg_traffic_links} \ No newline at end of file
diff --git a/resources/libraries/robot/counters.robot b/resources/libraries/robot/counters.robot
new file mode 100644
index 0000000000..a8897685ad
--- /dev/null
+++ b/resources/libraries/robot/counters.robot
@@ -0,0 +1,39 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Documentation | VPP counters keywords
+| Library | resources/libraries/python/VppCounters.py
+
+*** Keywords ***
+| Clear interface counters on all vpp nodes in topology
+| | [Documentation] | Clear interface counters on all VPP nodes in topology
+| | [Arguments] | ${nodes}
+| | Vpp Nodes Clear Interface Counters | ${nodes}
+
+| Vpp dump stats
+| | [Documentation] | Dump stats table on VPP node
+| | [Arguments] | ${node}
+| | Vpp Dump Stats Table | ${node}
+
+| Vpp get interface ipv6 counter
+| | [Documentation] | Return IPv6 statistics for node interface
+| | [Arguments] | ${node} | ${interface}
+| | ${ipv6_counter}= | Vpp Get Ipv6 Interface Counter | ${node} | ${interface}
+| | [Return] | ${ipv6_counter}
+
+| Check ipv4 interface counter
+| | [Documentation] | Check that ipv4 interface counter has right value
+| | [Arguments] | ${node} | ${interface} | ${value}
+| | ${ipv4_counter}= | Vpp get ipv4 interface counter | ${node} | ${interface}
+| | Should Be Equal | ${ipv4_counter} | ${value}
diff --git a/resources/libraries/robot/default.robot b/resources/libraries/robot/default.robot
new file mode 100644
index 0000000000..0c9341580e
--- /dev/null
+++ b/resources/libraries/robot/default.robot
@@ -0,0 +1,26 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Variables | resources/libraries/python/topology.py
+| Library | resources/libraries/python/DUTSetup.py
+| Library | resources/libraries/python/TGSetup.py
+
+*** Keywords ***
+| Setup all DUTs before test
+| | [Documentation] | Setup all DUTs in topology before test execution
+| | Setup All DUTs | ${nodes}
+
+| Setup all TGs before traffic script
+| | [Documentation] | Prepare all TGs before traffic scripts execution
+| | All TGs Set Interface Default Driver | ${nodes}
diff --git a/resources/libraries/robot/interfaces.robot b/resources/libraries/robot/interfaces.robot
new file mode 100644
index 0000000000..18c9c0cc81
--- /dev/null
+++ b/resources/libraries/robot/interfaces.robot
@@ -0,0 +1,20 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+*** Settings ***
+| Resource | resources/libraries/robot/vat/interfaces.robot
+
+*** Keywords ***
+| VPP reports interfaces on | [Arguments] | ${node}
+| | VPP reports interfaces through VAT on | ${node}
+#| | VPP reports interfaces through ODL on | ${node}
+#| | VPP reports interfaces through DEBUGCLI on | ${node}
diff --git a/resources/libraries/robot/ipv4.robot b/resources/libraries/robot/ipv4.robot
new file mode 100644
index 0000000000..a4e1086d38
--- /dev/null
+++ b/resources/libraries/robot/ipv4.robot
@@ -0,0 +1,51 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+*** Settings ***
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/counters.robot
+| Library | resources/libraries/python/IPv4Util.py
+| Variables | resources/libraries/python/IPv4NodeAddress.py
+
+*** Keywords ***
+
+| Setup IPv4 adresses on all nodes in topology
+| | [Documentation] | Setup IPv4 address on all DUTs and TG in topology
+| | [Arguments] | ${nodes} | ${nodes_addr}
+| | Nodes setup IPv4 addresses | ${nodes} | ${nodes_addr}
+
+| Interfaces needed for IPv4 testing are in "${state}" state
+| | Node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port1']['name']}" is in "${state}" state
+| | Node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port3']['name']}" is in "${state}" state
+| | Node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port1']['name']}" is in "${state}" state
+| | Node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port3']['name']}" is in "${state}" state
+
+| Routes are set up for IPv4 testing
+| | ${gateway} = | Get IPv4 address of node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port3']['name']}"
+| | ${subnet} = | Get IPv4 subnet of node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port1']['name']}"
+| | ${prefix_length} = | Get IPv4 address prefix of node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port1']['name']}"
+| | Node "${nodes['DUT1']}" routes to IPv4 network "${subnet}" with prefix length "${prefix_length}" using interface "${nodes['DUT1']['interfaces']['port3']['name']}" via "${gateway}"
+| | ${gateway} = | Get IPv4 address of node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port3']['name']}"
+| | ${subnet} = | Get IPv4 subnet of node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port1']['name']}"
+| | ${prefix_length} = | Get IPv4 address prefix of node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port1']['name']}"
+| | Node "${nodes['DUT2']}" routes to IPv4 network "${subnet}" with prefix length "${prefix_length}" using interface "${nodes['DUT2']['interfaces']['port3']['name']}" via "${gateway}"
+
+| Setup nodes for IPv4 testing
+| | Interfaces needed for IPv4 testing are in "up" state
+| | Setup IPv4 adresses on all nodes in topology | ${nodes} | ${nodes_ipv4_addr}
+| | Routes are set up for IPv4 testing
+
+| TG interface "${tg_port}" can route to node "${node}" interface "${port}" "${hops}" hops away using IPv4
+| | Node "${nodes['TG']}" interface "${tg_port}" can route to node "${node}" interface "${port}" "${hops}" hops away using IPv4
+
+| Node "${from_node}" interface "${from_port}" can route to node "${to_node}" interface "${to_port}" "${hops}" hops away using IPv4
+| | After ping is sent from node "${from_node}" interface "${from_port}" with destination IPv4 address of node "${to_node}" interface "${to_port}" a ping response arrives and TTL is decreased by "${hops}"
diff --git a/resources/libraries/robot/ipv6.robot b/resources/libraries/robot/ipv6.robot
new file mode 100644
index 0000000000..f45ba7c220
--- /dev/null
+++ b/resources/libraries/robot/ipv6.robot
@@ -0,0 +1,167 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""IPv6 keywords"""
+
+*** Settings ***
+| Library | resources/libraries/python/IPv6Util.py
+| Library | resources/libraries/python/IPv6Setup.py
+| Library | resources/libraries/python/TrafficScriptExecutor.py
+| Library | resources.libraries.python.topology.Topology
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/counters.robot
+| Documentation | IPv6 keywords
+
+*** Keywords ***
+| Ipv6 icmp echo
+| | [Documentation] | Type of the src_node must be TG and dst_node must be DUT
+| | [Arguments] | ${src_node} | ${dst_node} | ${nodes_addr}
+| | ${link}= | Get first active connecting link between node "${src_node}" and "${dst_node}"
+| | ${src_port}= | Get Interface By Link Name | ${src_node} | ${link}
+| | ${dst_port}= | Get Interface By Link Name | ${dst_node} | ${link}
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${src_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${dst_node} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${src_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${dst_node} | ${dst_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | icmpv6_echo.py | ${src_node} | ${args}
+| | Vpp dump stats | ${dst_node}
+| | ${ipv6_counter}= | Vpp get interface ipv6 counter | ${dst_node} | ${dst_port}
+| | Should Be Equal | ${ipv6_counter} | ${2} | #ICMPv6 neighbor advertisment + ICMPv6 echo request
+
+| Ipv6 icmp echo sweep
+| | [Documentation] | Type of the src_node must be TG and dst_node must be DUT
+| | [Arguments] | ${src_node} | ${dst_node} | ${nodes_addr}
+| | ${link}= | Get first active connecting link between node "${src_node}" and "${dst_node}"
+| | ${src_port}= | Get Interface By Link Name | ${src_node} | ${link}
+| | ${dst_port}= | Get Interface By Link Name | ${dst_node} | ${link}
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${src_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${dst_node} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${src_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${dst_node} | ${dst_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| # TODO: end_size is currently minimum MTU size for IPv6 minus IPv6 and ICMPv6
+| # echo header size, MTU info is not in VAT sw_interface_dump output
+| | ${args}= | Set Variable | ${args} --start_size 0 --end_size 1232 --step 1
+| | Run Traffic Script On Node | ipv6_sweep_ping.py | ${src_node} | ${args} | ${20}
+
+| Ipv6 tg to dut1 egress
+| | [Documentation] | Send traffic from TG to first DUT egress interface
+| | [Arguments] | ${tg_node} | ${first_dut} | ${nodes_addr}
+| | ${link}= | Get first active connecting link between node "${tg_node}" and "${first_dut}"
+| | ${src_port}= | Get Interface By Link Name | ${tg_node} | ${link}
+| | ${first_hop_port}= | Get Interface By Link Name | ${first_dut} | ${link}
+| | ${dst_port}= | Get first egress interface on "${first_dut}" for link with "${tg_node}"
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${first_dut} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${first_dut} | ${first_hop_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | icmpv6_echo.py | ${tg_node} | ${args}
+
+
+| Ipv6 tg to dut2 via dut1
+| | [Documentation] | Send traffic from TG to second DUT through first DUT
+| | [Arguments] | ${tg_node} | ${first_dut} | ${second_dut} | ${nodes_addr}
+| | ${link1}= | Get first active connecting link between node "${tg_node}" and "${first_dut}"
+| | ${src_port}= | Get Interface By Link Name | ${tg_node} | ${link1}
+| | ${first_hop_port}= | Get Interface By Link Name | ${first_dut} | ${link1}
+| | ${link2}= | Get first active connecting link between node "${first_dut}" and "${second_dut}"
+| | ${dst_port}= | Get Interface By Link Name | ${second_dut} | ${link2}
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${second_dut} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${first_dut} | ${first_hop_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | icmpv6_echo.py | ${tg_node} | ${args}
+
+| Ipv6 tg to dut2 egress via dut1
+| | [Documentation] | Send traffic from TG to second DUT egress interface through first DUT
+| | [Arguments] | ${tg_node} | ${first_dut} | ${second_dut} | ${nodes_addr}
+| | ${link}= | Get first active connecting link between node "${tg_node}" and "${first_dut}"
+| | ${src_port}= | Get Interface By Link Name | ${tg_node} | ${link}
+| | ${first_hop_port}= | Get Interface By Link Name | ${first_dut} | ${link}
+| | ${dst_port}= | Get first egress interface on "${first_dut}" for link with "${second_dut}"
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${second_dut} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${first_dut} | ${first_hop_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${src_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | icmpv6_echo.py | ${tg_node} | ${args}
+
+| Ipv6 tg to tg routed
+| | [Documentation] | Send traffic from one TG port to another through DUT nodes
+| | ... | and send reply back, also verify hop limit processing
+| | [Arguments] | ${tg_node} | ${first_dut} | ${second_dut} | ${nodes_addr}
+| | ${link1}= | Get first active connecting link between node "${tg_node}" and "${first_dut}"
+| | ${src_port}= | Get Interface By Link Name | ${tg_node} | ${link1}
+| | ${src_nh_port}= | Get Interface By Link Name | ${first_dut} | ${link1}
+| | ${link2}= | Get first active connecting link between node "${tg_node}" and "${second_dut}"
+| | ${dst_port}= | Get Interface By Link Name | ${tg_node} | ${link2}
+| | ${dst_nh_port}= | Get Interface By Link Name | ${second_dut} | ${link2}
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${src_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${dst_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${src_port}
+| | ${dst_mac}= | Get Interface Mac | ${tg_node} | ${dst_port}
+| | ${src_nh_mac}= | Get Interface Mac | ${first_dut} | ${src_nh_port}
+| | ${dst_nh_mac}= | Get Interface Mac | ${second_dut} | ${dst_nh_port}
+| | ${args}= | Traffic Script Gen Arg | ${src_port} | ${dst_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | ${args}= | Set Variable | ${args} --src_nh_mac ${src_nh_mac} --dst_nh_mac ${dst_nh_mac} --h_num 2
+| | Run Traffic Script On Node | icmpv6_echo_req_resp.py | ${tg_node} | ${args}
+
+| Ipv6 neighbor solicitation
+| | [Documentation] | Send IPv6 neighbor solicitation from TG to DUT
+| | [Arguments] | ${tg_node} | ${dut_node} | ${nodes_addr}
+| | ${link}= | Get first active connecting link between node "${tg_node}" and "${dut_node}"
+| | ${tg_port}= | Get Interface By Link Name | ${tg_node} | ${link}
+| | ${dut_port}= | Get Interface By Link Name | ${dut_node} | ${link}
+| | ${src_ip}= | Get Node Port Ipv6 Address | ${tg_node} | ${tg_port} | ${nodes_addr}
+| | ${dst_ip}= | Get Node Port Ipv6 Address | ${dut_node} | ${dut_port} | ${nodes_addr}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${tg_port}
+| | ${dst_mac}= | Get Interface Mac | ${dut_node} | ${dut_port}
+| | ${args}= | Traffic Script Gen Arg | ${tg_port} | ${tg_port} | ${src_mac}
+| | | ... | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | Run Traffic Script On Node | ipv6_ns.py | ${tg_node} | ${args}
+
+| Setup ipv6 to all dut in topology
+| | [Documentation] | Setup IPv6 address on all DUTs
+| | [Arguments] | ${nodes} | ${nodes_addr}
+| | Setup all DUTs before test
+| | Nodes Setup Ipv6 Addresses | ${nodes} | ${nodes_addr}
+
+| Clear ipv6 on all dut in topology
+| | [Documentation] | Remove IPv6 address on all DUTs
+| | [Arguments] | ${nodes} | ${nodes_addr}
+| | Nodes Clear Ipv6 Addresses | ${nodes} | ${nodes_addr}
+
+| Vpp nodes ra supress link layer
+| | [Documentation] | Supress ICMPv6 router advertisement message for link scope address
+| | [Arguments] | ${nodes}
+| | Vpp All Ra Supress Link Layer | ${nodes}
+
+| Vpp nodes setup ipv6 routing
+| | [Documentation] | Setup routing on all VPP nodes required for IPv6 tests
+| | [Arguments] | ${nodes} | ${nodes_addr}
+| | ${link_tg_dut1}= | Get first active connecting link between node "${nodes['TG']}" and "${nodes['DUT1']}"
+| | ${link_tg_dut2}= | Get first active connecting link between node "${nodes['TG']}" and "${nodes['DUT2']}"
+| | ${link_dut1_dut2}= | Get first active connecting link between node "${nodes['DUT1']}" and "${nodes['DUT2']}"
+| | ${dut1_if}= | Get Interface By Link Name | ${nodes['DUT1']} | ${link_dut1_dut2}
+| | ${dut2_if}= | Get Interface By Link Name | ${nodes['DUT2']} | ${link_dut1_dut2}
+| | Vpp Ipv6 Route Add | ${nodes['DUT1']} | ${link_tg_dut2} | ${dut1_if} | ${nodes_addr}
+| | Vpp Ipv6 Route Add | ${nodes['DUT2']} | ${link_tg_dut1} | ${dut2_if} | ${nodes_addr}
diff --git a/resources/libraries/robot/vat/interfaces.robot b/resources/libraries/robot/vat/interfaces.robot
new file mode 100644
index 0000000000..1342f6326b
--- /dev/null
+++ b/resources/libraries/robot/vat/interfaces.robot
@@ -0,0 +1,23 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+*** Settings ***
+| Library | resources/libraries/python/VatExecutor.py
+
+*** Variables ***
+| ${VAT_DUMP_INTERFACES} | dump_interfaces.vat
+
+*** Keywords ***
+| VPP reports interfaces through VAT on
+| | [Arguments] | ${node}
+| | Execute Script | ${VAT_DUMP_INTERFACES} | ${node}
+| | Script Should Have Passed
diff --git a/resources/templates/vat/add_ip_address.vat b/resources/templates/vat/add_ip_address.vat
new file mode 100644
index 0000000000..d59480c33a
--- /dev/null
+++ b/resources/templates/vat/add_ip_address.vat
@@ -0,0 +1 @@
+sw_interface_add_del_address sw_if_index {sw_if_index} {address}/{prefix_length}
diff --git a/resources/templates/vat/add_route.vat b/resources/templates/vat/add_route.vat
new file mode 100644
index 0000000000..e580854254
--- /dev/null
+++ b/resources/templates/vat/add_route.vat
@@ -0,0 +1 @@
+ip_add_del_route {network}/{prefix_length} via {gateway} sw_if_index {sw_if_index} resolve-attempts 1
diff --git a/resources/templates/vat/clear_interface.vat b/resources/templates/vat/clear_interface.vat
new file mode 100644
index 0000000000..aa9a4e7db3
--- /dev/null
+++ b/resources/templates/vat/clear_interface.vat
@@ -0,0 +1,3 @@
+exec clear interface
+quit
+
diff --git a/resources/templates/vat/del_ip_address.vat b/resources/templates/vat/del_ip_address.vat
new file mode 100644
index 0000000000..667ced2757
--- /dev/null
+++ b/resources/templates/vat/del_ip_address.vat
@@ -0,0 +1 @@
+sw_interface_add_del_address sw_if_index {sw_if_index} {address}/{prefix_length} del
diff --git a/resources/templates/vat/del_route.vat b/resources/templates/vat/del_route.vat
new file mode 100644
index 0000000000..e7fe4bc1e1
--- /dev/null
+++ b/resources/templates/vat/del_route.vat
@@ -0,0 +1 @@
+ip_add_del_route {network}/{prefix_length} via {gateway} sw_if_index {sw_if_index} del \ No newline at end of file
diff --git a/resources/templates/vat/dump_interfaces.vat b/resources/templates/vat/dump_interfaces.vat
new file mode 100644
index 0000000000..dfc5e6939d
--- /dev/null
+++ b/resources/templates/vat/dump_interfaces.vat
@@ -0,0 +1,3 @@
+sw_interface_dump
+dump_interface_table
+quit
diff --git a/resources/templates/vat/flush_ip_addresses.vat b/resources/templates/vat/flush_ip_addresses.vat
new file mode 100644
index 0000000000..f38fcf12cb
--- /dev/null
+++ b/resources/templates/vat/flush_ip_addresses.vat
@@ -0,0 +1 @@
+sw_interface_add_del_address sw_if_index {sw_if_index} del-all \ No newline at end of file
diff --git a/resources/templates/vat/l2_bridge_domain.vat b/resources/templates/vat/l2_bridge_domain.vat
new file mode 100644
index 0000000000..84bf409944
--- /dev/null
+++ b/resources/templates/vat/l2_bridge_domain.vat
@@ -0,0 +1,5 @@
+bridge_domain_add_del bd_id 1 flood 1 uu-flood 1 forward 1 learn 1 arp-term 0
+sw_interface_set_l2_bridge {interface1} bd_id 1 shg 0 enable
+sw_interface_set_l2_bridge {interface2} bd_id 1 shg 0 enable
+sw_interface_set_flags {interface1} admin-up link-up
+sw_interface_set_flags {interface2} admin-up link-up \ No newline at end of file
diff --git a/resources/templates/vat/l2_bridge_domain_gen.vat b/resources/templates/vat/l2_bridge_domain_gen.vat
new file mode 100644
index 0000000000..4e635e29c4
--- /dev/null
+++ b/resources/templates/vat/l2_bridge_domain_gen.vat
@@ -0,0 +1,5 @@
+bridge_domain_add_del bd_id 1 flood 1 uu-flood 1 forward 1 learn 1 arp-term 0
+sw_interface_set_l2_bridge TenGigabitEthernet84/0/1 bd_id 1 shg 0 enable
+sw_interface_set_l2_bridge TenGigabitEthernet84/0/0 bd_id 1 shg 0 enable
+sw_interface_set_flags TenGigabitEthernet84/0/1 admin-up link-up
+sw_interface_set_flags TenGigabitEthernet84/0/0 admin-up link-up \ No newline at end of file
diff --git a/resources/templates/vat/l2xconnect.vat b/resources/templates/vat/l2xconnect.vat
new file mode 100644
index 0000000000..8059007f17
--- /dev/null
+++ b/resources/templates/vat/l2xconnect.vat
@@ -0,0 +1,6 @@
+exec set interface state TenGigabitEthernet84/0/0 up
+exec set interface state TenGigabitEthernet84/0/1 up
+exec set interface l2 xconnect TenGigabitEthernet84/0/0 TenGigabitEthernet84/0/1
+exec set interface l2 xconnect TenGigabitEthernet84/0/1 TenGigabitEthernet84/0/0
+quit
+
diff --git a/resources/templates/vat/set_if_state.vat b/resources/templates/vat/set_if_state.vat
new file mode 100644
index 0000000000..e2c2d4b29d
--- /dev/null
+++ b/resources/templates/vat/set_if_state.vat
@@ -0,0 +1 @@
+sw_interface_set_flags sw_if_index {sw_if_index} {state}
diff --git a/resources/topology_schemas/3_node_topology.sch.yaml b/resources/topology_schemas/3_node_topology.sch.yaml
new file mode 100644
index 0000000000..da5c368bc6
--- /dev/null
+++ b/resources/topology_schemas/3_node_topology.sch.yaml
@@ -0,0 +1,49 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 defines required nodes for 3-node topology.
+
+name: 3_node_topology
+
+# +------+ +-----+ port1
+# | | port1 port3 | +------+
+# | DUT1 +-----------------+ TG | |
+# | +-----------------+ +------+
+# | | port2 port4 | | port2
+# +-+-+--+ +-+-+-+
+# port3 | | port4 port5 | | port6
+# | | | |
+# | | | |
+# port3 | | port4 | |
+# +-+-+--+ | |
+# | | port1 | |
+# | DUT2 +-------------------+ |
+# | +---------------------+
+# | | port2
+# +------+
+
+type: map
+mapping:
+ metadata:
+ include: topology_metadata_map
+
+ nodes:
+ type: map
+ required: yes
+ mapping:
+ TG:
+ include: type_tg
+ DUT1:
+ include: type_dut
+ DUT2:
+ include: type_dut
diff --git a/resources/topology_schemas/topology.sch.yaml b/resources/topology_schemas/topology.sch.yaml
new file mode 100644
index 0000000000..b69cd2df02
--- /dev/null
+++ b/resources/topology_schemas/topology.sch.yaml
@@ -0,0 +1,113 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 defines yaml schema for topolgy yaml.
+
+schema;topology_metadata_map:
+ type: map
+ mapping:
+ version:
+ type: any
+ schema:
+ required: yes
+ type: seq
+ sequence:
+ - type: str
+ unique: True
+ tags:
+ include: list_tags
+ check_script:
+ type: str
+ start_script:
+ type: str
+
+
+schema;list_tags:
+ type: seq
+ sequence:
+ - type: str
+ unique: True
+
+schema;type_interfaces:
+ type: map
+ mapping: &type_interface_mapping
+ regex;(port\d+): &type_interface_mapping_port
+ type: map
+ mapping: &type_interface_mapping_port_mapping
+ name:
+ type: str
+ pci_address:
+ type: str
+ pattern: "[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\\.\\d{1}"
+ mac_address:
+ type: str
+ pattern: "[0-9a-f]{2}(:[0-9a-f]{2}){5}"
+ link:
+ type: str
+ required: yes
+ driver: &type_interface_mapping_driver
+ type: str
+
+schema;type_interface_tg: &type_interface_tg
+ type: map
+ mapping:
+ <<: *type_interface_mapping
+ regex;(port\d+):
+ <<: *type_interface_mapping_port
+ mapping:
+ <<: *type_interface_mapping_port_mapping
+ driver:
+ <<: *type_interface_mapping_driver
+ required: yes
+
+schema;type_node: &type_node
+ type: map
+ mapping: &type_node_mapping
+ type: &type_node_mapping_type
+ required: yes
+ type: str
+ host:
+ required: yes
+ type: str
+ port:
+ type: int
+ username:
+ type: str
+ password:
+ type: str
+ priv_key:
+ type: str
+ interfaces:
+ type: map
+ mapping:
+ <<: *type_interface_mapping
+
+schema;type_tg:
+ type: map
+ mapping:
+ <<: *type_node_mapping
+ type:
+ <<: *type_node_mapping_type
+ enum: [TG]
+ interfaces:
+ <<: *type_interface_tg
+
+schema;type_dut:
+ type: map
+ mapping:
+ <<: *type_node_mapping
+ type:
+ <<: *type_node_mapping_type
+ enum: [DUT]
+
+# vim: sw=2:sts=2
diff --git a/resources/traffic_scripts/icmpv6_echo.py b/resources/traffic_scripts/icmpv6_echo.py
new file mode 100755
index 0000000000..c3c8d5a381
--- /dev/null
+++ b/resources/traffic_scripts/icmpv6_echo.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script for ICMPv6 echo test."""
+
+import sys
+import logging
+logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+from scapy.layers.inet6 import IPv6, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr
+from scapy.layers.inet6 import ICMPv6EchoRequest, ICMPv6EchoReply
+from scapy.all import Ether
+
+
+def main():
+ args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip'])
+
+ rxq = RxQueue(args.get_arg('rx_if'))
+ txq = TxQueue(args.get_arg('tx_if'))
+
+ src_mac = args.get_arg('src_mac')
+ dst_mac = args.get_arg('dst_mac')
+ src_ip = args.get_arg('src_ip')
+ dst_ip = args.get_arg('dst_ip')
+ echo_id = 0xa
+ echo_seq = 0x1
+
+ sent_packets = []
+
+ # send ICMPv6 neighbor advertisement message
+ pkt_send = (Ether(src=src_mac, dst='ff:ff:ff:ff:ff:ff') /
+ IPv6(src=src_ip, dst='ff02::1:ff00:2') /
+ ICMPv6ND_NA(tgt=src_ip, R=0) /
+ ICMPv6NDOptDstLLAddr(lladdr=src_mac))
+ sent_packets.append(pkt_send)
+ txq.send(pkt_send)
+
+ # send ICMPv6 echo request
+ pkt_send = (Ether(src=src_mac, dst=dst_mac) /
+ IPv6(src=src_ip, dst=dst_ip) /
+ ICMPv6EchoRequest(id=echo_id, seq=echo_seq))
+ sent_packets.append(pkt_send)
+ txq.send(pkt_send)
+
+ # receive ICMPv6 echo reply
+ ether = rxq.recv(2, sent_packets)
+ if ether is None:
+ rxq._proc.terminate()
+ raise RuntimeError('ICMPv6 echo reply Rx timeout')
+
+ if not ether.haslayer(IPv6):
+ rxq._proc.terminate()
+ raise RuntimeError('Unexpected packet with no IPv6 received {0}'.format(
+ ether.__repr__()))
+
+ ipv6 = ether['IPv6']
+
+ if not ipv6.haslayer(ICMPv6EchoReply):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no IPv6 ICMP received {0}'.format(
+ ipv6.__repr__()))
+
+ icmpv6 = ipv6['ICMPv6 Echo Reply']
+
+ # check identifier and sequence number
+ if icmpv6.id != echo_id or icmpv6.seq != echo_seq:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid ICMPv6 echo reply received ID {0} seq {1} should be ' +
+ 'ID {2} seq {3}'.format(icmpv6.id, icmpv6.seq, echo_id, echo_seq))
+
+ # verify checksum
+ cksum = icmpv6.cksum
+ del icmpv6.cksum
+ tmp = ICMPv6EchoReply(str(icmpv6))
+ if tmp.cksum != cksum:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid checksum {0} should be {1}'.format(cksum, tmp.cksum))
+
+ rxq._proc.terminate()
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/resources/traffic_scripts/icmpv6_echo_req_resp.py b/resources/traffic_scripts/icmpv6_echo_req_resp.py
new file mode 100755
index 0000000000..24f4faa3f4
--- /dev/null
+++ b/resources/traffic_scripts/icmpv6_echo_req_resp.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Send ICMPv6 echo request from one TG port to another through DUT nodes and
+ send reply back. Also verify hop limit processing."""
+
+import sys
+import logging
+logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+from scapy.layers.inet6 import IPv6, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr
+from scapy.layers.inet6 import ICMPv6EchoRequest, ICMPv6EchoReply
+from scapy.all import Ether
+
+
+def main():
+ args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_nh_mac', 'dst_nh_mac',
+ 'src_ip', 'dst_ip', 'h_num'])
+
+ src_rxq = RxQueue(args.get_arg('rx_if'))
+ src_txq = TxQueue(args.get_arg('rx_if'))
+ dst_rxq = RxQueue(args.get_arg('tx_if'))
+ dst_txq = TxQueue(args.get_arg('tx_if'))
+
+ src_mac = args.get_arg('src_mac')
+ dst_mac = args.get_arg('dst_mac')
+ src_nh_mac = args.get_arg('src_nh_mac')
+ dst_nh_mac = args.get_arg('dst_nh_mac')
+ src_ip = args.get_arg('src_ip')
+ dst_ip = args.get_arg('dst_ip')
+ hop_num = int(args.get_arg('h_num'))
+ hop_limit = 64
+ echo_id = 0xa
+ echo_seq = 0x1
+
+ src_sent_packets = []
+ dst_sent_packets = []
+
+ # send ICMPv6 neighbor advertisement message
+ pkt_send = (Ether(src=src_mac, dst='ff:ff:ff:ff:ff:ff') /
+ IPv6(src=src_ip, dst='ff02::1:ff00:2') /
+ ICMPv6ND_NA(tgt=src_ip, R=0) /
+ ICMPv6NDOptDstLLAddr(lladdr=src_mac))
+ src_sent_packets.append(pkt_send)
+ src_txq.send(pkt_send)
+ pkt_send = (Ether(src=dst_mac, dst='ff:ff:ff:ff:ff:ff') /
+ IPv6(src=dst_ip, dst='ff02::1:ff00:2') /
+ ICMPv6ND_NA(tgt=dst_ip, R=0) /
+ ICMPv6NDOptDstLLAddr(lladdr=dst_mac))
+ dst_sent_packets.append(pkt_send)
+ dst_txq.send(pkt_send)
+
+ # send ICMPv6 echo request from first TG interface
+ pkt_send = (Ether(src=src_mac, dst=src_nh_mac) /
+ IPv6(src=src_ip, dst=dst_ip, hlim=hop_limit) /
+ ICMPv6EchoRequest(id=echo_id, seq=echo_seq))
+ src_sent_packets.append(pkt_send)
+ src_txq.send(pkt_send)
+
+ # receive ICMPv6 echo request on second TG interface
+ ether = dst_rxq.recv(2, dst_sent_packets)
+ if ether is None:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError('ICMPv6 echo reply Rx timeout')
+
+ if not ether.haslayer(IPv6):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError('Unexpected packet with no IPv6 received {0}'.format(
+ ether.__repr__()))
+
+ ipv6 = ether['IPv6']
+
+ # verify hop limit processing
+ if ipv6.hlim != (hop_limit - hop_num):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid hop limit {0} should be {1}'.format(ipv6.hlim,
+ hop_limit - hop_num))
+
+ if not ipv6.haslayer(ICMPv6EchoRequest):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no IPv6 ICMP received {0}'.format(
+ ipv6.__repr__()))
+
+ icmpv6 = ipv6['ICMPv6 Echo Request']
+
+ # check identifier and sequence number
+ if icmpv6.id != echo_id or icmpv6.seq != echo_seq:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid ICMPv6 echo reply received ID {0} seq {1} should be ' +
+ 'ID {2} seq {3}'.format(icmpv6.id, icmpv6.seq, echo_id, echo_seq))
+
+ # verify checksum
+ cksum = icmpv6.cksum
+ del icmpv6.cksum
+ tmp = ICMPv6EchoRequest(str(icmpv6))
+ if tmp.cksum != cksum:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid checksum {0} should be {1}'.format(cksum, tmp.cksum))
+
+ # send ICMPv6 echo reply from second TG interface
+ pkt_send = (Ether(src=dst_mac, dst=dst_nh_mac) /
+ IPv6(src=dst_ip, dst=src_ip) /
+ ICMPv6EchoReply(id=echo_id, seq=echo_seq))
+ dst_sent_packets.append(pkt_send)
+ dst_txq.send(pkt_send)
+
+ # receive ICMPv6 echo reply on first TG interface
+ ether = src_rxq.recv(2, src_sent_packets)
+ if ether is None:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError('ICMPv6 echo reply Rx timeout')
+
+ if not ether.haslayer(IPv6):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError('Unexpected packet with no IPv6 received {0}'.format(
+ ether.__repr__()))
+
+ ipv6 = ether['IPv6']
+
+ # verify hop limit processing
+ if ipv6.hlim != (hop_limit - hop_num):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid hop limit {0} should be {1}'.format(ipv6.hlim,
+ hop_limit - hop_num))
+
+ if not ipv6.haslayer(ICMPv6EchoReply):
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no IPv6 ICMP received {0}'.format(
+ ipv6.__repr__()))
+
+ icmpv6 = ipv6['ICMPv6 Echo Reply']
+
+ # check identifier and sequence number
+ if icmpv6.id != echo_id or icmpv6.seq != echo_seq:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid ICMPv6 echo reply received ID {0} seq {1} should be ' +
+ 'ID {2} seq {3}'.format(icmpv6.id, icmpv6.seq, echo_id, echo_seq))
+
+ # verify checksum
+ cksum = icmpv6.cksum
+ del icmpv6.cksum
+ tmp = ICMPv6EchoReply(str(icmpv6))
+ if tmp.cksum != cksum:
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid checksum {0} should be {1}'.format(cksum, tmp.cksum))
+
+ src_rxq._proc.terminate()
+ dst_rxq._proc.terminate()
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/resources/traffic_scripts/ipv4_ping_ttl_check.py b/resources/traffic_scripts/ipv4_ping_ttl_check.py
new file mode 100755
index 0000000000..050a1d7b29
--- /dev/null
+++ b/resources/traffic_scripts/ipv4_ping_ttl_check.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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 scapy.all import *
+from resources.libraries.python.PacketVerifier \
+ import Interface, create_gratuitous_arp_request, auto_pad
+from optparse import OptionParser
+
+
+def check_ttl(ttl_begin, ttl_end, ttl_diff):
+ if ttl_begin != ttl_end + ttl_diff:
+ src_if.close()
+ if dst_if_defined:
+ dst_if.close()
+ raise Exception(
+ "TTL changed from {} to {} but decrease by {} expected")\
+ .format(ttl_begin, ttl_end, hops)
+
+
+def ckeck_packets_equal(pkt_send, pkt_recv):
+ pkt_send_raw = str(pkt_send)
+ pkt_recv_raw = str(pkt_recv)
+ if pkt_send_raw != pkt_recv_raw:
+ print "Sent: {}".format(pkt_send_raw.encode('hex'))
+ print "Received: {}".format(pkt_recv_raw.encode('hex'))
+ print "Sent:"
+ Ether(pkt_send_raw).show2()
+ print "Received:"
+ Ether(pkt_recv_raw).show2()
+ src_if.close()
+ if dst_if_defined:
+ dst_if.close()
+ raise Exception("Sent packet doesn't match received packet")
+
+
+parser = OptionParser()
+parser.add_option("--src_if", dest="src_if")
+parser.add_option("--dst_if", dest="dst_if") # optional
+parser.add_option("--src_mac", dest="src_mac")
+parser.add_option("--first_hop_mac", dest="first_hop_mac")
+parser.add_option("--dst_mac", dest="dst_mac") # optional
+parser.add_option("--src_ip", dest="src_ip")
+parser.add_option("--dst_ip", dest="dst_ip")
+parser.add_option("--hops", dest="hops") # optional
+# If one of 'dst_if', 'dst_mac' and 'hops' is specified all must be specified.
+(opts, args) = parser.parse_args()
+src_if_name = opts.src_if
+dst_if_name = opts.dst_if
+dst_if_defined = True
+if dst_if_name is None:
+ dst_if_defined = False
+src_mac = opts.src_mac
+first_hop_mac = opts.first_hop_mac
+dst_mac = opts.dst_mac
+src_ip = opts.src_ip
+dst_ip = opts.dst_ip
+hops = int(opts.hops)
+
+if dst_if_defined and (src_if_name == dst_if_name):
+ raise Exception("Source interface name equals destination interface name")
+
+src_if = Interface(src_if_name)
+src_if.send_pkt(create_gratuitous_arp_request(src_mac, src_ip))
+if dst_if_defined:
+ dst_if = Interface(dst_if_name)
+ dst_if.send_pkt(create_gratuitous_arp_request(dst_mac, dst_ip))
+
+pkt_req_send = auto_pad(Ether(src=src_mac, dst=first_hop_mac) /
+ IP(src=src_ip, dst=dst_ip) /
+ ICMP())
+pkt_req_send = Ether(pkt_req_send)
+src_if.send_pkt(pkt_req_send)
+
+if dst_if_defined:
+ try:
+ pkt_req_recv = dst_if.recv_pkt()
+ except:
+ src_if.close()
+ if dst_if_defined:
+ dst_if.close()
+ raise
+
+ check_ttl(pkt_req_send[IP].ttl, pkt_req_recv[IP].ttl, hops)
+ pkt_req_send_mod = pkt_req_send.copy()
+ pkt_req_send_mod[IP].ttl = pkt_req_recv[IP].ttl
+ del pkt_req_send_mod[IP].chksum # update checksum
+ ckeck_packets_equal(pkt_req_send_mod[IP], pkt_req_recv[IP])
+
+ pkt_resp_send = auto_pad(Ether(src=dst_mac, dst=pkt_req_recv.src) /
+ IP(src=dst_ip, dst=src_ip) /
+ ICMP(type=0)) # echo-reply
+ pkt_resp_send = Ether(pkt_resp_send)
+ dst_if.send_pkt(pkt_resp_send)
+
+try:
+ pkt_resp_recv = src_if.recv_pkt()
+except:
+ src_if.close()
+ if dst_if_defined:
+ dst_if.close()
+ raise
+
+if dst_if_defined:
+ check_ttl(pkt_resp_send[IP].ttl, pkt_resp_recv[IP].ttl, hops)
+ pkt_resp_send_mod = pkt_resp_send.copy()
+ pkt_resp_send_mod[IP].ttl = pkt_resp_recv[IP].ttl
+ del pkt_resp_send_mod[IP].chksum # update checksum
+ ckeck_packets_equal(pkt_resp_send_mod[IP], pkt_resp_recv[IP])
+
+src_if.close()
+if dst_if_defined:
+ dst_if.close()
diff --git a/resources/traffic_scripts/ipv6_ns.py b/resources/traffic_scripts/ipv6_ns.py
new file mode 100755
index 0000000000..dd1adad39e
--- /dev/null
+++ b/resources/traffic_scripts/ipv6_ns.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script for IPv6 Neighbor Solicitation test."""
+
+import sys
+import logging
+logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+from scapy.layers.inet6 import IPv6, ICMPv6ND_NA, ICMPv6ND_NS
+from scapy.layers.inet6 import ICMPv6NDOptDstLLAddr, ICMPv6NDOptSrcLLAddr
+from scapy.all import Ether
+
+
+def main():
+ args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip'])
+
+ rxq = RxQueue(args.get_arg('rx_if'))
+ txq = TxQueue(args.get_arg('tx_if'))
+
+ src_mac = args.get_arg('src_mac')
+ dst_mac = args.get_arg('dst_mac')
+ src_ip = args.get_arg('src_ip')
+ dst_ip = args.get_arg('dst_ip')
+
+ sent_packets = []
+
+ # send ICMPv6 neighbor solicitation message
+ pkt_send = (Ether(src=src_mac, dst='ff:ff:ff:ff:ff:ff') /
+ IPv6(src=src_ip, dst='ff02::1:ff00:2') /
+ ICMPv6ND_NS(tgt=dst_ip) /
+ ICMPv6NDOptSrcLLAddr(lladdr=src_mac))
+ sent_packets.append(pkt_send)
+ txq.send(pkt_send)
+
+ # receive ICMPv6 neighbor advertisement message
+ ether = rxq.recv(2, sent_packets)
+ if ether is None:
+ rxq._proc.terminate()
+ raise RuntimeError('ICMPv6 echo reply Rx timeout')
+
+ if not ether.haslayer(IPv6):
+ rxq._proc.terminate()
+ raise RuntimeError('Unexpected packet with no IPv6 received {0}'.format(
+ ether.__repr__()))
+
+ ipv6 = ether['IPv6']
+
+ if not ipv6.haslayer(ICMPv6ND_NA):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no ICMPv6 ND-NA received {0}'.format(
+ ipv6.__repr__()))
+
+ icmpv6_na = ipv6['ICMPv6 Neighbor Discovery - Neighbor Advertisement']
+
+ # verify target address
+ if icmpv6_na.tgt != dst_ip:
+ rxq._proc.terminate()
+ raise RuntimeError('Invalid target address {0} should be {1}'.format(
+ icmpv6_na.tgt, dst_ip))
+
+ if not icmpv6_na.haslayer(ICMPv6NDOptDstLLAddr):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Missing Destination Link-Layer Address option in ICMPv6 ' +
+ 'Neighbor Advertisement {0}'.format(icmpv6_na.__repr__()))
+
+ option = 'ICMPv6 Neighbor Discovery Option - Destination Link-Layer Address'
+ dst_ll_addr = icmpv6_na[option]
+
+ # verify destination link-layer address field
+ if dst_ll_addr.lladdr != dst_mac:
+ rxq._proc.terminate()
+ raise RuntimeError('Invalid lladdr {0} should be {1}'.format(
+ dst_ll_addr.lladdr, dst_mac))
+
+ # verify checksum
+ cksum = icmpv6_na.cksum
+ del icmpv6_na.cksum
+ tmp = ICMPv6ND_NA(str(icmpv6_na))
+ if tmp.cksum != cksum:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid checksum {0} should be {1}'.format(cksum, tmp.cksum))
+
+ rxq._proc.terminate()
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/resources/traffic_scripts/ipv6_sweep_ping.py b/resources/traffic_scripts/ipv6_sweep_ping.py
new file mode 100755
index 0000000000..2282f40f78
--- /dev/null
+++ b/resources/traffic_scripts/ipv6_sweep_ping.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script for IPv6 sweep ping."""
+
+import sys
+import logging
+import os
+logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+from scapy.layers.inet6 import IPv6, ICMPv6ND_NA, ICMPv6NDOptDstLLAddr
+from scapy.layers.inet6 import ICMPv6EchoRequest, ICMPv6EchoReply
+from scapy.all import Ether
+
+
+def main():
+ # start_size - start size of the ICMPv6 echo data
+ # end_size - end size of the ICMPv6 echo data
+ # step - increment step
+ args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip',
+ 'start_size', 'end_size', 'step'])
+
+ rxq = RxQueue(args.get_arg('rx_if'))
+ txq = TxQueue(args.get_arg('tx_if'))
+
+ src_mac = args.get_arg('src_mac')
+ dst_mac = args.get_arg('dst_mac')
+ src_ip = args.get_arg('src_ip')
+ dst_ip = args.get_arg('dst_ip')
+ start_size = int(args.get_arg('start_size'))
+ end_size = int(args.get_arg('end_size'))
+ step = int(args.get_arg('step'))
+ echo_id = 0xa
+ # generate some random data buffer
+ data = bytearray(os.urandom(end_size))
+
+ # send ICMPv6 neighbor advertisement message
+ sent_packets = []
+ pkt_send = (Ether(src=src_mac, dst=dst_mac) /
+ IPv6(src=src_ip, dst=dst_ip) /
+ ICMPv6ND_NA(tgt=src_ip, R=0) /
+ ICMPv6NDOptDstLLAddr(lladdr=src_mac))
+ sent_packets.append(pkt_send)
+ txq.send(pkt_send)
+
+ # send ICMPv6 echo request with incremented data length and receive ICMPv6
+ # echo reply
+ for echo_seq in range(start_size, end_size, step):
+ pkt_send = (Ether(src=src_mac, dst=dst_mac) /
+ IPv6(src=src_ip, dst=dst_ip) /
+ ICMPv6EchoRequest(id=echo_id, seq=echo_seq,
+ data=data[0:echo_seq]))
+ sent_packets.append(pkt_send)
+ txq.send(pkt_send)
+
+ ether = rxq.recv(ignore=sent_packets)
+ if ether is None:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'ICMPv6 echo reply seq {0} Rx timeout'.format(echo_seq))
+
+ if not ether.haslayer(IPv6):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no IPv6 received {0}'.format(
+ ether.__repr__()))
+
+ ipv6 = ether['IPv6']
+
+ if not ipv6.haslayer(ICMPv6EchoReply):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Unexpected packet with no IPv6 ICMP received {0}'.format(
+ ipv6.__repr__()))
+
+ icmpv6 = ipv6['ICMPv6 Echo Reply']
+
+ if icmpv6.id != echo_id or icmpv6.seq != echo_seq:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid ICMPv6 echo reply received ID {0} seq {1} should be ' +
+ 'ID {2} seq {3}, {0}'.format(icmpv6.id, icmpv6.seq, echo_id,
+ echo_seq))
+
+ cksum = icmpv6.cksum
+ del icmpv6.cksum
+ tmp = ICMPv6EchoReply(str(icmpv6))
+ if tmp.cksum != cksum:
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Invalid checksum {0} should be {1}'.format(cksum, tmp.cksum))
+
+ rxq._proc.terminate()
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/resources/traffic_scripts/send_ip_icmp.py b/resources/traffic_scripts/send_ip_icmp.py
new file mode 100755
index 0000000000..fd15376fb1
--- /dev/null
+++ b/resources/traffic_scripts/send_ip_icmp.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script that sends an ip icmp packet
+from one interface to the other"""
+
+import sys
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+from scapy.layers.inet import ICMP, IP
+from scapy.all import Ether
+
+
+def main():
+ """ Send IP icmp packet from one traffic generator interface to the other"""
+ args = TrafficScriptArg(['src_mac', 'dst_mac', 'src_ip', 'dst_ip'])
+
+ src_mac = args.get_arg('src_mac')
+ dst_mac = args.get_arg('dst_mac')
+ src_ip = args.get_arg('src_ip')
+ dst_ip = args.get_arg('dst_ip')
+ tx_if = args.get_arg('tx_if')
+ rx_if = args.get_arg('rx_if')
+
+ rxq = RxQueue(rx_if)
+ txq = TxQueue(tx_if)
+
+ sent_packets = []
+
+ # Create empty ip ICMP packet and add padding before sending
+ pkt_raw = Ether(src=src_mac, dst=dst_mac) / \
+ IP(src=src_ip, dst=dst_ip) / \
+ ICMP()
+
+ # Send created packet on one interface and receive on the other
+ sent_packets.append(pkt_raw)
+ txq.send(pkt_raw)
+
+ ether = rxq.recv(1)
+
+ # Check whether received packet contains layers Ether, IP and ICMP
+ if ether is None:
+ rxq._proc.terminate()
+ raise RuntimeError('ICMPv6 echo reply Rx timeout')
+
+ if not ether.haslayer(IP):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Not an IP packet received {0}'.format(ether.__repr__()))
+
+ if not ether.haslayer(ICMP):
+ rxq._proc.terminate()
+ raise RuntimeError(
+ 'Not an ICMP packet received {0}'.format(ether.__repr__()))
+
+ rxq._proc.terminate()
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/suites/__init__.robot b/tests/suites/__init__.robot
new file mode 100644
index 0000000000..fc3c810e39
--- /dev/null
+++ b/tests/suites/__init__.robot
@@ -0,0 +1,20 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Resource | resources/libraries/robot/default.robot
+| Library | resources/libraries/python/SetupFramework.py
+| Library | resources.libraries.python.topology.Topology
+| Suite Setup | Run Keywords | Setup Framework | ${nodes}
+| ... | AND | Update All Interface Data On All Nodes | ${nodes}
+
diff --git a/tests/suites/bridge_domain/test.robot b/tests/suites/bridge_domain/test.robot
new file mode 100644
index 0000000000..a36e5928b6
--- /dev/null
+++ b/tests/suites/bridge_domain/test.robot
@@ -0,0 +1,39 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+*** Settings ***
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/interfaces.robot
+| Resource | resources/libraries/robot/bridge_domain.robot
+| Test Setup | Setup all DUTs before test
+| Library | resources.libraries.python.topology.Topology
+| Variables | resources/libraries/python/topology.py
+| Force Tags | 3_NODE_DOUBLE_LINK_TOPO
+
+*** Test Cases ***
+
+| VPP reports interfaces
+| | VPP reports interfaces on | ${nodes['DUT1']}
+
+| Vpp forwards packets via L2 bridge domain 2 ports
+| | [Tags] | 3_NODE_DOUBLE_LINK_TOPO
+| | ${TG_DUT_links}= | Get active links connecting "${nodes['TG']}" and "${nodes['DUT1']}"
+| | Setup l2 bridge on node "${nodes['DUT1']}" via links "${TG_DUT_links}"
+| | Send traffic on node "${nodes['TG']}" from link "${TG_DUT_links[0]}" to link "${TG_DUT_links[1]}"
+
+| Vpp forwards packets via L2 bridge domain in circular topology
+| | [Tags] | 3_NODE_DOUBLE_LINK_TOPO
+| | ${tg}= | Set Variable | ${nodes['TG']}
+| | ${dut1}= | Set Variable | ${nodes['DUT1']}
+| | ${dut2}= | Set Variable | ${nodes['DUT2']}
+| | ${tg_links}= | Setup TG "${tg}" DUT1 "${dut1}" and DUT2 "${dut2}" for 3 node l2 bridge domain test
+| | Send traffic on node "${nodes['TG']}" from link "${tg_links[0]}" to link "${tg_links[1]}"
diff --git a/tests/suites/ipv4/ipv4_untagged.robot b/tests/suites/ipv4/ipv4_untagged.robot
new file mode 100644
index 0000000000..dde2e5bb67
--- /dev/null
+++ b/tests/suites/ipv4/ipv4_untagged.robot
@@ -0,0 +1,58 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Library | resources.libraries.python.topology.Topology
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/ipv4.robot
+| Suite Setup | Run Keywords | Setup all DUTs before test
+| ... | AND | Update All Interface Data On All Nodes | ${nodes}
+| ... | AND | Setup nodes for IPv4 testing
+| Test Setup | Clear interface counters on all vpp nodes in topology | ${nodes}
+
+*** Test Cases ***
+
+| VPP replies to ICMPv4 echo request
+| | TG interface "${nodes['TG']['interfaces']['port3']['name']}" can route to node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port1']['name']}" "0" hops away using IPv4
+| | Vpp dump stats table | ${nodes['DUT1']}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port1']['name']} | ${1}
+
+| TG can route to DUT egress interface
+| | TG interface "${nodes['TG']['interfaces']['port3']['name']}" can route to node "${nodes['DUT1']}" interface "${nodes['DUT1']['interfaces']['port3']['name']}" "0" hops away using IPv4
+| | Vpp dump stats table | ${nodes['DUT1']}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port1']['name']} | ${1}
+
+| TG can route to DUT2 through DUT1
+| | TG interface "${nodes['TG']['interfaces']['port3']['name']}" can route to node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port3']['name']}" "1" hops away using IPv4
+| | Vpp dump stats table | ${nodes['DUT1']}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port1']['name']} | ${1}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port3']['name']} | ${1}
+| | Vpp dump stats table | ${nodes['DUT2']}
+| | Check ipv4 interface counter | ${nodes['DUT2']} | ${nodes['DUT2']['interfaces']['port3']['name']} | ${1}
+
+| TG can route to DUT2 egress interface through DUT1
+| | TG interface "${nodes['TG']['interfaces']['port3']['name']}" can route to node "${nodes['DUT2']}" interface "${nodes['DUT2']['interfaces']['port1']['name']}" "1" hops away using IPv4
+| | Vpp dump stats table | ${nodes['DUT1']}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port1']['name']} | ${1}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port3']['name']} | ${1}
+| | Vpp dump stats table | ${nodes['DUT2']}
+| | Check ipv4 interface counter | ${nodes['DUT2']} | ${nodes['DUT2']['interfaces']['port3']['name']} | ${1}
+
+| TG can route to TG through DUT1 and DUT2
+| | TG interface "${nodes['TG']['interfaces']['port3']['name']}" can route to node "${nodes['TG']}" interface "${nodes['TG']['interfaces']['port5']['name']}" "2" hops away using IPv4
+| | Vpp dump stats table | ${nodes['DUT1']}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port1']['name']} | ${1}
+| | Check ipv4 interface counter | ${nodes['DUT1']} | ${nodes['DUT1']['interfaces']['port3']['name']} | ${1}
+| | Vpp dump stats table | ${nodes['DUT2']}
+| | Check ipv4 interface counter | ${nodes['DUT2']} | ${nodes['DUT2']['interfaces']['port3']['name']} | ${1}
+| | Check ipv4 interface counter | ${nodes['DUT2']} | ${nodes['DUT2']['interfaces']['port1']['name']} | ${1}
diff --git a/tests/suites/ipv6/ipv6_untagged.robot b/tests/suites/ipv6/ipv6_untagged.robot
new file mode 100644
index 0000000000..e437ae618a
--- /dev/null
+++ b/tests/suites/ipv6/ipv6_untagged.robot
@@ -0,0 +1,52 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""IPv6 untagged test suite"""
+
+*** Settings ***
+| Documentation | IPv6 untagged test suite
+| Resource | resources/libraries/robot/ipv6.robot
+| Resource | resources/libraries/robot/counters.robot
+| Resource | resources/libraries/robot/default.robot
+| Variables | resources/libraries/python/IPv6NodesAddr.py | ${nodes}
+| Suite Setup | Run Keywords | Setup ipv6 to all dut in topology | ${nodes} | ${nodes_ipv6_addr}
+| ... | AND | Vpp nodes ra supress link layer | ${nodes}
+| ... | AND | Vpp nodes setup ipv6 routing | ${nodes} | ${nodes_ipv6_addr}
+| ... | AND | Setup all TGs before traffic script
+| Suite Teardown | Clear ipv6 on all dut in topology | ${nodes} | ${nodes_ipv6_addr}
+| Test Setup | Clear interface counters on all vpp nodes in topology | ${nodes}
+
+*** Test Cases ***
+| VPP replies to ICMPv6 echo request
+| | Ipv6 icmp echo | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes_ipv6_addr}
+
+| VPP can process ICMPv6 echo request from min to max packet size with 1B increment
+| | Ipv6 icmp echo sweep | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes_ipv6_addr}
+
+| TG can route to first DUT egress interface
+| | Ipv6 tg to dut1 egress | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes_ipv6_addr}
+
+| TG can route to second DUT through first DUT
+| | Ipv6 tg to dut2 via dut1 | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']}
+| | ... | ${nodes_ipv6_addr}
+
+| TG can route to second DUT egress interface through first DUT
+| | Ipv6 tg to dut2 egress via dut1 | ${nodes['TG']} | ${nodes['DUT1']}
+| | ... | ${nodes['DUT2']} | ${nodes_ipv6_addr}
+
+| TG can route to TG through first and second DUT
+| | Ipv6 tg to tg routed | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']}
+| | ... | ${nodes_ipv6_addr}
+
+| VPP replies to IPv6 Neighbor Solicitation
+| | Ipv6 neighbor solicitation | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes_ipv6_addr}
diff --git a/tests/suites/performance/short.robot b/tests/suites/performance/short.robot
new file mode 100644
index 0000000000..5c04d5ec81
--- /dev/null
+++ b/tests/suites/performance/short.robot
@@ -0,0 +1,41 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+*** Settings ***
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/interfaces.robot
+| Library | resources/libraries/python/VatExecutor.py
+| Library | resources/libraries/python/TrafficGenerator.py
+| Force Tags | topo-3node
+| Test Setup | Setup all DUTs before test
+
+*** Test Cases ***
+| VPP passes traffic through L2 cross connect
+| | Given L2 xconnect initialized in topology
+| | Then Traffic should pass with no loss | 10 | 10 | 512
+
+*** Keywords ***
+| L2 xconnect initialized in topology
+| | Setup L2 xconnect | ${nodes['DUT1']} | port1 | port2
+| | Setup L2 xconnect | ${nodes['DUT2']} | port1 | port2
+
+
+| Setup L2 xconnect | [Arguments] | ${node} | ${src_port} | ${dst_port}
+| | Execute script | l2xconnect.vat | ${node}
+| | Script should have passed
+
+
+| Traffic should pass with no loss
+| | [Arguments] | ${duration} | ${rate} | ${framesize}
+| | Send traffic on | ${nodes['TG']} | port1 | port2 | ${duration}
+| | ... | ${rate} | ${framesize}
+| | No traffic loss occured
diff --git a/topologies/available/3_node_hw_topo1.yaml.example b/topologies/available/3_node_hw_topo1.yaml.example
new file mode 100644
index 0000000000..01fc9aae31
--- /dev/null
+++ b/topologies/available/3_node_hw_topo1.yaml.example
@@ -0,0 +1,69 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+# Example file of topology
+
+---
+metadata:
+ version: 0.1
+ schema: # list of schema files against which to validate
+ - resources/topology_schemas/3_node_topology.sch.yaml
+ - resources/topology_schemas/topology.sch.yaml
+ tags: [hw, 3-node]
+
+nodes:
+ TG:
+ type: TG
+ host: 10.0.0.3
+ port: 22
+ username: lab
+ password: lab
+ interfaces:
+ port3:
+ mac_address: "08:00:27:35:59:04"
+ pci_address: "0000:00:08.0"
+ link: link1
+ port5:
+ mac_address: "08:00:27:46:2b:4c"
+ pci_address: "0000:00:09.0"
+ link: link2
+ DUT1:
+ type: DUT
+ host: 10.0.0.1
+ port: 22
+ username: lab
+ password: lab
+ interfaces:
+ port1:
+ mac_address: "08:00:27:ae:29:2b"
+ pci_address: "0000:00:08.0"
+ link: link1
+ port3:
+ mac_address: "08:00:27:f3:be:f0"
+ pci_address: "0000:00:09.0"
+ link: link3
+ DUT2:
+ type: DUT
+ host: 10.0.0.2
+ port: 22
+ username: lab
+ password: lab
+ interfaces:
+ port1:
+ mac_address: "08:00:27:f2:90:d8"
+ pci_address: "0000:00:08.0"
+ link: link2
+ port3:
+ mac_address: "08:00:27:14:64:e0"
+ pci_address: "0000:00:09.0"
+ link: link3
diff --git a/topologies/available/README b/topologies/available/README
new file mode 100644
index 0000000000..dfb556afbb
--- /dev/null
+++ b/topologies/available/README
@@ -0,0 +1 @@
+Define available topologies. \ No newline at end of file
diff --git a/topologies/enabled/README b/topologies/enabled/README
new file mode 100644
index 0000000000..3a55e0e5d2
--- /dev/null
+++ b/topologies/enabled/README
@@ -0,0 +1,2 @@
+To enable topology in testing, simlink available topology.
+ ln -s ../available/topology.yaml \ No newline at end of file