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