New version of RF tests. 07/107/11
authorStefan Kobza <skobza@cisco.com>
Mon, 11 Jan 2016 17:03:25 +0000 (18:03 +0100)
committerStefan Kobza <skobza@cisco.com>
Mon, 8 Feb 2016 21:38:32 +0000 (22:38 +0100)
Change-Id: I241a2b7a7706e65f71cfd4a62e2a40f053fc5d07
Signed-off-by: Stefan Kobza <skobza@cisco.com>
69 files changed:
.gitignore [new file with mode: 0644]
.gitreview [new file with mode: 0644]
README [new file with mode: 0644]
bootstrap.sh [new file with mode: 0755]
docs/tag_documentation.rst [new file with mode: 0644]
docs/topology_schemas [new file with mode: 0644]
main.py [new file with mode: 0755]
pylint.cfg [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
resources/__init__.py [new file with mode: 0644]
resources/libraries/__init__.py [new file with mode: 0644]
resources/libraries/bash/dut_setup.sh [new file with mode: 0644]
resources/libraries/python/DUTSetup.py [new file with mode: 0644]
resources/libraries/python/IPUtil.py [new file with mode: 0644]
resources/libraries/python/IPv4NodeAddress.py [new file with mode: 0644]
resources/libraries/python/IPv4Util.py [new file with mode: 0644]
resources/libraries/python/IPv6NodesAddr.py [new file with mode: 0644]
resources/libraries/python/IPv6Setup.py [new file with mode: 0644]
resources/libraries/python/IPv6Util.py [new file with mode: 0644]
resources/libraries/python/InterfaceSetup.py [new file with mode: 0644]
resources/libraries/python/PacketVerifier.py [new file with mode: 0644]
resources/libraries/python/SetupFramework.py [new file with mode: 0644]
resources/libraries/python/TGSetup.py [new file with mode: 0644]
resources/libraries/python/TrafficGenerator.py [new file with mode: 0644]
resources/libraries/python/TrafficScriptArg.py [new file with mode: 0644]
resources/libraries/python/TrafficScriptExecutor.py [new file with mode: 0644]
resources/libraries/python/VatConfigGenerator.py [new file with mode: 0644]
resources/libraries/python/VatExecutor.py [new file with mode: 0644]
resources/libraries/python/VppCounters.py [new file with mode: 0644]
resources/libraries/python/__init__.py [new file with mode: 0644]
resources/libraries/python/constants.py [new file with mode: 0644]
resources/libraries/python/parsers/JsonParser.py [new file with mode: 0644]
resources/libraries/python/parsers/__init__.py [new file with mode: 0644]
resources/libraries/python/ssh.py [new file with mode: 0644]
resources/libraries/python/topology.py [new file with mode: 0644]
resources/libraries/robot/bridge_domain.robot [new file with mode: 0644]
resources/libraries/robot/counters.robot [new file with mode: 0644]
resources/libraries/robot/default.robot [new file with mode: 0644]
resources/libraries/robot/interfaces.robot [new file with mode: 0644]
resources/libraries/robot/ipv4.robot [new file with mode: 0644]
resources/libraries/robot/ipv6.robot [new file with mode: 0644]
resources/libraries/robot/vat/interfaces.robot [new file with mode: 0644]
resources/templates/vat/add_ip_address.vat [new file with mode: 0644]
resources/templates/vat/add_route.vat [new file with mode: 0644]
resources/templates/vat/clear_interface.vat [new file with mode: 0644]
resources/templates/vat/del_ip_address.vat [new file with mode: 0644]
resources/templates/vat/del_route.vat [new file with mode: 0644]
resources/templates/vat/dump_interfaces.vat [new file with mode: 0644]
resources/templates/vat/flush_ip_addresses.vat [new file with mode: 0644]
resources/templates/vat/l2_bridge_domain.vat [new file with mode: 0644]
resources/templates/vat/l2_bridge_domain_gen.vat [new file with mode: 0644]
resources/templates/vat/l2xconnect.vat [new file with mode: 0644]
resources/templates/vat/set_if_state.vat [new file with mode: 0644]
resources/topology_schemas/3_node_topology.sch.yaml [new file with mode: 0644]
resources/topology_schemas/topology.sch.yaml [new file with mode: 0644]
resources/traffic_scripts/icmpv6_echo.py [new file with mode: 0755]
resources/traffic_scripts/icmpv6_echo_req_resp.py [new file with mode: 0755]
resources/traffic_scripts/ipv4_ping_ttl_check.py [new file with mode: 0755]
resources/traffic_scripts/ipv6_ns.py [new file with mode: 0755]
resources/traffic_scripts/ipv6_sweep_ping.py [new file with mode: 0755]
resources/traffic_scripts/send_ip_icmp.py [new file with mode: 0755]
tests/suites/__init__.robot [new file with mode: 0644]
tests/suites/bridge_domain/test.robot [new file with mode: 0644]
tests/suites/ipv4/ipv4_untagged.robot [new file with mode: 0644]
tests/suites/ipv6/ipv6_untagged.robot [new file with mode: 0644]
tests/suites/performance/short.robot [new file with mode: 0644]
topologies/available/3_node_hw_topo1.yaml.example [new file with mode: 0644]
topologies/available/README [new file with mode: 0644]
topologies/enabled/README [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..304828a
--- /dev/null
@@ -0,0 +1,8 @@
+/env
+outputs
+output.xml
+log.html
+report.html
+*.pyc
+*~
+*.log
diff --git a/.gitreview b/.gitreview
new file mode 100644 (file)
index 0000000..edd22cc
--- /dev/null
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.fd.io
+port=29418
+project=csit.git
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..f3d9397
--- /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 (executable)
index 0000000..835759c
--- /dev/null
@@ -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 (file)
index 0000000..188f3b1
--- /dev/null
@@ -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 (file)
index 0000000..d25e99a
--- /dev/null
@@ -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 (executable)
index 0000000..d575567
--- /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 (file)
index 0000000..3762258
--- /dev/null
@@ -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 (file)
index 0000000..6146c4b
--- /dev/null
@@ -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 (file)
index 0000000..83c9fbf
--- /dev/null
@@ -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 (file)
index 0000000..73d02f6
--- /dev/null
@@ -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 (file)
index 0000000..dc36a08
--- /dev/null
@@ -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 (file)
index 0000000..76f76ae
--- /dev/null
@@ -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 (file)
index 0000000..3e002b3
--- /dev/null
@@ -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 (file)
index 0000000..0f2c1d9
--- /dev/null
@@ -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 (file)
index 0000000..5480bfc
--- /dev/null
@@ -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 (file)
index 0000000..33192b8
--- /dev/null
@@ -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 (file)
index 0000000..45a8eba
--- /dev/null
@@ -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 (file)
index 0000000..a96683b
--- /dev/null
@@ -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 (file)
index 0000000..9b60435
--- /dev/null
@@ -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 (file)
index 0000000..81798e1
--- /dev/null
@@ -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 (file)
index 0000000..47c609f
--- /dev/null
@@ -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 (file)
index 0000000..3e372e9
--- /dev/null
@@ -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 (file)
index 0000000..d86917a
--- /dev/null
@@ -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 (file)
index 0000000..ab76f29
--- /dev/null
@@ -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 (file)
index 0000000..2e65a52
--- /dev/null
@@ -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 (file)
index 0000000..98be9d3
--- /dev/null
@@ -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 (file)
index 0000000..5582a86
--- /dev/null
@@ -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 (file)
index 0000000..f34d7a7
--- /dev/null
@@ -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 (file)
index 0000000..16058f3
--- /dev/null
@@ -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 (file)
index 0000000..d7134ce
--- /dev/null
@@ -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 (file)
index 0000000..1d17767
--- /dev/null
@@ -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 (file)
index 0000000..5a0e0e1
--- /dev/null
@@ -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 (file)
index 0000000..72e41c7
--- /dev/null
@@ -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 (file)
index 0000000..522de37
--- /dev/null
@@ -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 (file)
index 0000000..fc37057
--- /dev/null
@@ -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 (file)
index 0000000..a889768
--- /dev/null
@@ -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 (file)
index 0000000..0c93415
--- /dev/null
@@ -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 (file)
index 0000000..18c9c0c
--- /dev/null
@@ -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 (file)
index 0000000..a4e1086
--- /dev/null
@@ -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 (file)
index 0000000..f45ba7c
--- /dev/null
@@ -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 (file)
index 0000000..1342f63
--- /dev/null
@@ -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 (file)
index 0000000..d59480c
--- /dev/null
@@ -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 (file)
index 0000000..e580854
--- /dev/null
@@ -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 (file)
index 0000000..aa9a4e7
--- /dev/null
@@ -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 (file)
index 0000000..667ced2
--- /dev/null
@@ -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 (file)
index 0000000..e7fe4bc
--- /dev/null
@@ -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 (file)
index 0000000..dfc5e69
--- /dev/null
@@ -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 (file)
index 0000000..f38fcf1
--- /dev/null
@@ -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 (file)
index 0000000..84bf409
--- /dev/null
@@ -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 (file)
index 0000000..4e635e2
--- /dev/null
@@ -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 (file)
index 0000000..8059007
--- /dev/null
@@ -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 (file)
index 0000000..e2c2d4b
--- /dev/null
@@ -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 (file)
index 0000000..da5c368
--- /dev/null
@@ -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 (file)
index 0000000..b69cd2d
--- /dev/null
@@ -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 (executable)
index 0000000..c3c8d5a
--- /dev/null
@@ -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 (executable)
index 0000000..24f4faa
--- /dev/null
@@ -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 (executable)
index 0000000..050a1d7
--- /dev/null
@@ -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 (executable)
index 0000000..dd1adad
--- /dev/null
@@ -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 (executable)
index 0000000..2282f40
--- /dev/null
@@ -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 (executable)
index 0000000..fd15376
--- /dev/null
@@ -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 (file)
index 0000000..fc3c810
--- /dev/null
@@ -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 (file)
index 0000000..a36e592
--- /dev/null
@@ -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 (file)
index 0000000..dde2e5b
--- /dev/null
@@ -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 (file)
index 0000000..e437ae6
--- /dev/null
@@ -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 (file)
index 0000000..5c04d5e
--- /dev/null
@@ -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 (file)
index 0000000..01fc9aa
--- /dev/null
@@ -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 (file)
index 0000000..dfb556a
--- /dev/null
@@ -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 (file)
index 0000000..3a55e0e
--- /dev/null
@@ -0,0 +1,2 @@
+To enable topology in testing, simlink available topology.
+  ln -s ../available/topology.yaml
\ No newline at end of file