From f56b77a0764222cc45a9df572df901067a273356 Mon Sep 17 00:00:00 2001 From: Damjan Marion Date: Mon, 3 Oct 2016 19:44:57 +0200 Subject: test: new test infrastructure Change-Id: I73ca19c431743f6b39669c583d9222a6559346ef Signed-off-by: Jan Gelety Signed-off-by: Juraj Sloboda Signed-off-by: Stefan Kobza Signed-off-by: Matej Klotton Signed-off-by: Maciek Konstantynowicz Signed-off-by: Damjan Marion --- test/util.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 test/util.py (limited to 'test/util.py') diff --git a/test/util.py b/test/util.py new file mode 100644 index 00000000..c72a3965 --- /dev/null +++ b/test/util.py @@ -0,0 +1,139 @@ +## @package util +# Module with common functions that should be used by the test cases. +# +# The module provides a set of tools for setup the test environment + +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6NDOptSrcLLAddr + + +## Util class +# +# Test cases that want to use methods defined in Util class should +# inherit this class. +# +# class Example(Util, VppTestCase): +# pass +class Util(object): + + ## Class method to send ARP Request for each VPP IPv4 address in + # order to determine VPP interface MAC address to IPv4 bindings. + # + # Resolved MAC address is saved to the VPP_MACS dictionary with interface + # index as a key. ARP Request is sent from MAC in MY_MACS dictionary with + # interface index as a key. + # @param cls The class pointer. + # @param args List variable to store indices of VPP interfaces. + @classmethod + def resolve_arp(cls, args): + for i in args: + ip = cls.VPP_IP4S[i] + cls.log("Sending ARP request for %s on port %u" % (ip, i)) + arp_req = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / + ARP(op=ARP.who_has, pdst=ip, + psrc=cls.MY_IP4S[i], hwsrc=cls.MY_MACS[i])) + cls.pg_add_stream(i, arp_req) + cls.pg_enable_capture([i]) + + cls.cli(2, "trace add pg-input 1") + cls.pg_start() + arp_reply = cls.pg_get_capture(i)[0] + if arp_reply[ARP].op == ARP.is_at: + cls.log("VPP pg%u MAC address is %s " % (i, arp_reply[ARP].hwsrc)) + cls.VPP_MACS[i] = arp_reply[ARP].hwsrc + else: + cls.log("No ARP received on port %u" % i) + cls.cli(2, "show trace") + ## @var ip + # + ## @var arp_req + # + ## @var arp_reply + # + ## @var VPP_MACS + # + + ## Class method to send ND request for each VPP IPv6 address in + # order to determine VPP MAC address to IPv6 bindings. + # + # Resolved MAC address is saved to the VPP_MACS dictionary with interface + # index as a key. ND Request is sent from MAC in MY_MACS dictionary with + # interface index as a key. + # @param cls The class pointer. + # @param args List variable to store indices of VPP interfaces. + @classmethod + def resolve_icmpv6_nd(cls, args): + for i in args: + ip = cls.VPP_IP6S[i] + cls.log("Sending ICMPv6ND_NS request for %s on port %u" % (ip, i)) + nd_req = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / + IPv6(src=cls.MY_IP6S[i], dst=ip) / + ICMPv6ND_NS(tgt=ip) / + ICMPv6NDOptSrcLLAddr(lladdr=cls.MY_MACS[i])) + cls.pg_add_stream(i, nd_req) + cls.pg_enable_capture([i]) + + cls.cli(2, "trace add pg-input 1") + cls.pg_start() + nd_reply = cls.pg_get_capture(i)[0] + icmpv6_na = nd_reply['ICMPv6 Neighbor Discovery - Neighbor Advertisement'] + dst_ll_addr = icmpv6_na['ICMPv6 Neighbor Discovery Option - Destination Link-Layer Address'] + cls.VPP_MACS[i] = dst_ll_addr.lladdr + ## @var ip + # + ## @var nd_req + # + ## @var nd_reply + # + ## @var icmpv6_na + # + ## @var dst_ll_addr + # + ## @var VPP_MACS + # + + ## Class method to configure IPv4 addresses on VPP interfaces. + # + # Set dictionary variables MY_IP4S and VPP_IP4S to IPv4 addresses + # calculated using interface VPP interface index as a parameter. + # /24 IPv4 prefix is used, with VPP interface address host part set + # to .1 and MY address set to .2. + # Used IPv4 prefix scheme: 172.16.{VPP-interface-index}.0/24. + # @param cls The class pointer. + # @param args List variable to store indices of VPP interfaces. + @classmethod + def config_ip4(cls, args): + for i in args: + cls.MY_IP4S[i] = "172.16.%u.2" % i + cls.VPP_IP4S[i] = "172.16.%u.1" % i + cls.api("sw_interface_add_del_address pg%u %s/24" % (i, cls.VPP_IP4S[i])) + cls.log("My IPv4 address is %s" % (cls.MY_IP4S[i])) + ## @var MY_IP4S + # Dictionary variable to store host IPv4 addresses connected to packet + # generator interfaces. + ## @var VPP_IP4S + # Dictionary variable to store VPP IPv4 addresses of the packet + # generator interfaces. + + ## Class method to configure IPv6 addresses on VPP interfaces. + # + # Set dictionary variables MY_IP6S and VPP_IP6S to IPv6 addresses + # calculated using interface VPP interface index as a parameter. + # /64 IPv6 prefix is used, with VPP interface address host part set + # to ::1 and MY address set to ::2. + # Used IPv6 prefix scheme: fd10:{VPP-interface-index}::0/64. + # @param cls The class pointer. + # @param args List variable to store indices of VPP interfaces. + @classmethod + def config_ip6(cls, args): + for i in args: + cls.MY_IP6S[i] = "fd10:%u::2" % i + cls.VPP_IP6S[i] = "fd10:%u::1" % i + cls.api("sw_interface_add_del_address pg%u %s/64" % (i, cls.VPP_IP6S[i])) + cls.log("My IPv6 address is %s" % (cls.MY_IP6S[i])) + ## @var MY_IP6S + # Dictionary variable to store host IPv6 addresses connected to packet + # generator interfaces. + ## @var VPP_IP6S + # Dictionary variable to store VPP IPv6 addresses of the packet + # generator interfaces. -- cgit 1.2.3-korg From f62ae1288a776527c7f7ba3951531fbd07bc63da Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 11 Oct 2016 11:47:09 +0200 Subject: refactor test framework Change-Id: I31da3b1857b6399f9899276a2d99cdd19436296c Signed-off-by: Klement Sekera Signed-off-by: Matej Klotton Signed-off-by: Jan Gelety Signed-off-by: Juraj Sloboda --- Makefile | 41 ++- build-root/Makefile | 1 + test/Makefile | 23 +- test/doc/Makefile | 229 ++++++++++++ test/doc/conf.py | 340 ++++++++++++++++++ test/doc/index.rst | 22 ++ test/framework.py | 873 ++++++++++++++++++++-------------------------- test/hook.py | 128 +++++++ test/template_bd.py | 120 +++---- test/test_ip.py | 302 ++++++---------- test/test_ip6.py | 323 ++++++----------- test/test_l2bd.py | 539 ++++++++-------------------- test/test_l2xc.py | 276 ++++++--------- test/test_lb.py | 252 ++++++------- test/test_vxlan.py | 106 +++--- test/util.py | 152 +------- test/vpp_interface.py | 240 +++++++++++++ test/vpp_papi_provider.py | 316 +++++++++++++++++ test/vpp_pg_interface.py | 99 ++++++ test/vpp_sub_interface.py | 143 ++++++++ 20 files changed, 2658 insertions(+), 1867 deletions(-) create mode 100644 test/doc/Makefile create mode 100644 test/doc/conf.py create mode 100644 test/doc/index.rst create mode 100644 test/hook.py create mode 100644 test/vpp_interface.py create mode 100644 test/vpp_papi_provider.py create mode 100644 test/vpp_pg_interface.py create mode 100644 test/vpp_sub_interface.py (limited to 'test/util.py') diff --git a/Makefile b/Makefile index 93277bec..54b0f29d 100644 --- a/Makefile +++ b/Makefile @@ -188,37 +188,36 @@ plugins-release: $(BR)/.bootstrap.ok build-vpp-api: $(BR)/.bootstrap.ok $(call make,$(PLATFORM)_debug,vpp-api-install) +PYTHON_PATH=$(BR)/python + define test - @make -C test \ - VPP_TEST_BIN=$(BR)/install-$(1)-native/vpp/bin/vpp \ - VPP_TEST_API_TEST_BIN=$(BR)/install-$(1)-native/vpp-api-test/bin/vpp_api_test \ - VPP_TEST_PLUGIN_PATH=$(BR)/install-$(1)-native/plugins/lib64/vpp_plugins \ - V=$(V) TEST=$(TEST) + $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install vpp-api-test-install,) + make -C test \ + VPP_TEST_BIN=$(BR)/install-$(2)-native/vpp/bin/vpp \ + VPP_TEST_API_TEST_BIN=$(BR)/install-$(2)-native/vpp-api-test/bin/vpp_api_test \ + VPP_TEST_PLUGIN_PATH=$(BR)/install-$(2)-native/plugins/lib64/vpp_plugins \ + LD_LIBRARY_PATH=$(BR)/install-$(2)-native/vpp-api/lib64/ \ + WS_ROOT=$(WS_ROOT) I=$(I) V=$(V) TEST=$(TEST) PYTHON_PATH=$(PYTHON_PATH) $(3) endef test: bootstrap -ifeq ($(OS_ID),ubuntu) - @if ! (dpkg -l python-dev python-scapy &> /dev/null); then \ - sudo -E apt-get $(CONFIRM) $(FORCE) install python-dev python-scapy; \ - fi -endif - @make -C $(BR) PLATFORM=vpp_lite TAG=vpp_lite vpp-api-install plugins-install vpp-install vpp-api-test-install - $(call test,vpp_lite) + $(call test,vpp_lite,vpp_lite,test) test-debug: bootstrap -ifeq ($(OS_ID),ubuntu) - @if ! (dpkg -l python-dev python-scapy &> /dev/null); then \ - sudo -E apt-get $(CONFIRM) $(FORCE) install python-dev python-scapy; \ - fi -endif - @make -C $(BR) PLATFORM=vpp_lite TAG=vpp_lite_debug vpp-api-install plugins-install vpp-install vpp-api-test-install - $(call test,vpp_lite_debug) + $(call test,vpp_lite,vpp_lite_debug,test) + +test-doc: + make -C $(BR) PLATFORM=vpp_lite TAG=vpp_lite vpp-api-install plugins-install vpp-install vpp-api-test-install + make -C test PYTHON_PATH=$(PYTHON_PATH) LD_LIBRARY_PATH=$(BR)/install-vpp_lite-native/vpp-api/lib64/ doc + +test-clean: + make -C test clean retest: - $(call test,vpp_lite) + $(call test,vpp_lite,vpp_lite,retest) retest-debug: - $(call test,vpp_lite_debug) + $(call test,vpp_lite,vpp_lite_debug,retest) STARTUP_DIR ?= $(PWD) ifeq ("$(wildcard $(STARTUP_CONF))","") diff --git a/build-root/Makefile b/build-root/Makefile index 97fb43d4..6e26e90e 100644 --- a/build-root/Makefile +++ b/build-root/Makefile @@ -1164,6 +1164,7 @@ distclean: rm -rf $(MU_BUILD_ROOT_DIR)/*.deb rm -rf $(MU_BUILD_ROOT_DIR)/*.rpm rm -rf $(MU_BUILD_ROOT_DIR)/*.changes + rm -rf $(MU_BUILD_ROOT_DIR)/python if [ -e /usr/bin/dh ];then (cd $(MU_BUILD_ROOT_DIR)/deb/;debian/rules clean); fi rm -f $(MU_BUILD_ROOT_DIR)/deb/debian/*.install rm -f $(MU_BUILD_ROOT_DIR)/deb/debian/*.dkms diff --git a/test/Makefile b/test/Makefile index 7cbcf97a..c90679e8 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,3 +1,22 @@ +PYTHON_VENV_PATH=$(PYTHON_PATH)/virtualenv -all: - @python run_tests.py discover -p test_$(TEST)"*.py" +test: clean + @virtualenv $(PYTHON_VENV_PATH) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install scapy" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install pexpect" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && cd $(WS_ROOT)/vpp-api/python && python setup.py install" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" + +retest: clean + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" + +.PHONY: clean doc + +clean: + @rm -f /dev/shm/vpp-unittest-* + @rm -rf /tmp/vpp-unittest-* + +doc: + @virtualenv $(PYTHON_VENV_PATH) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc html" diff --git a/test/doc/Makefile b/test/doc/Makefile new file mode 100644 index 00000000..00655389 --- /dev/null +++ b/test/doc/Makefile @@ -0,0 +1,229 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: regen-api-doc +regen-api-doc: + sphinx-apidoc -o . .. + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: regen-api-doc + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: regen-api-doc + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: regen-api-doc + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: regen-api-doc + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: regen-api-doc + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: regen-api-doc + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: regen-api-doc + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/VPPtestframework.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VPPtestframework.qhc" + +.PHONY: applehelp +applehelp: regen-api-doc + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: regen-api-doc + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/VPPtestframework" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/VPPtestframework" + @echo "# devhelp" + +.PHONY: epub +epub: regen-api-doc + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: regen-api-doc + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: regen-api-doc + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: regen-api-doc + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: regen-api-doc + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: regen-api-doc + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: regen-api-doc + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: regen-api-doc + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: regen-api-doc + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: regen-api-doc + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: regen-api-doc + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: regen-api-doc + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: regen-api-doc + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: regen-api-doc + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: regen-api-doc + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: regen-api-doc + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/test/doc/conf.py b/test/doc/conf.py new file mode 100644 index 00000000..aa841714 --- /dev/null +++ b/test/doc/conf.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +# +# VPP test framework documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 13 08:45:03 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'VPP test framework' +copyright = u'2016, VPP team' +author = u'VPP team' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = u'VPP test framework v0.1' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'VPPtestframeworkdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'VPPtestframework.tex', u'VPP test framework Documentation', + u'VPP team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'vpptestframework', u'VPP test framework Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'VPPtestframework', u'VPP test framework Documentation', + author, 'VPPtestframework', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/test/doc/index.rst b/test/doc/index.rst new file mode 100644 index 00000000..c18357a4 --- /dev/null +++ b/test/doc/index.rst @@ -0,0 +1,22 @@ +.. VPP test framework documentation master file, created by + sphinx-quickstart on Thu Oct 13 08:45:03 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to VPP test framework's documentation! +============================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + modules.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/test/framework.py b/test/framework.py index 5cf2a250..02ffb7ad 100644 --- a/test/framework.py +++ b/test/framework.py @@ -1,424 +1,315 @@ #!/usr/bin/env python -## @package framework -# Module to handle test case execution. -# -# The module provides a set of tools for constructing and running tests and -# representing the results. - -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) +from abc import * import os import sys import subprocess import unittest +import tempfile +import resource +from time import sleep from inspect import getdoc +from hook import PollHook +from vpp_pg_interface import VppPGInterface +from vpp_papi_provider import VppPapiProvider -from scapy.utils import wrpcap, rdpcap from scapy.packet import Raw -## Static variables to store color formatting strings. +from logging import * + +""" + Test framework module. + + The module provides a set of tools for constructing and running tests and + representing the results. +""" + +handler = StreamHandler(sys.stdout) +getLogger().addHandler(handler) +try: + verbose = int(os.getenv("V", 0)) +except: + verbose = 0 +# 40 = ERROR, 30 = WARNING, 20 = INFO, 10 = DEBUG, 0 = NOTSET (all messages) +getLogger().setLevel(40 - 10 * verbose) +getLogger("scapy.runtime").addHandler(handler) +getLogger("scapy.runtime").setLevel(ERROR) + +# Static variables to store color formatting strings. # -# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure -# the color of the text to be printed in the terminal. Variable END is used -# to revert the text color to the default one. +# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure +# the color of the text to be printed in the terminal. Variable COLOR_RESET +# is used to revert the text color to the default one. if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' LPURPLE = '\033[94m' - END = '\033[0m' + COLOR_RESET = '\033[0m' else: RED = '' GREEN = '' YELLOW = '' LPURPLE = '' - END = '' + COLOR_RESET = '' + + +""" @var formatting delimiter consisting of '=' characters """ +double_line_delim = '=' * 70 +""" @var formatting delimiter consisting of '-' characters """ +single_line_delim = '-' * 70 -## Private class to create packet info object. -# -# Help process information about the next packet. -# Set variables to default values. class _PacketInfo(object): + """Private class to create packet info object. + + Help process information about the next packet. + Set variables to default values. + @property index + Integer variable to store the index of the packet. + @property src + Integer variable to store the index of the source packet generator + interface of the packet. + @property dst + Integer variable to store the index of the destination packet generator + interface of the packet. + @property data + Object variable to store the copy of the former packet. + + + """ index = -1 src = -1 dst = -1 data = None - ## @var index - # Integer variable to store the index of the packet. - ## @var src - # Integer variable to store the index of the source packet generator - # interface of the packet. - ## @var dst - # Integer variable to store the index of the destination packet generator - # interface of the packet. - ## @var data - # Object variable to store the copy of the former packet. - -## Subclass of the python unittest.TestCase class. -# -# This subclass is a base class for test cases that are implemented as classes. -# It provides methods to create and run test case. + + class VppTestCase(unittest.TestCase): + """ + Subclass of the python unittest.TestCase class. + + This subclass is a base class for test cases that are implemented as classes + It provides methods to create and run test case. + + """ + + @property + def packet_infos(self): + """List of packet infos""" + return self._packet_infos + + @packet_infos.setter + def packet_infos(self, value): + self._packet_infos = value + + @classmethod + def instance(cls): + """Return the instance of this testcase""" + return cls.test_instance - ## Class method to set class constants necessary to run test case. - # @param cls The class pointer. @classmethod def setUpConstants(cls): + """ Set-up the test case class based on environment variables """ + try: + cls.interactive = True if int(os.getenv("I")) > 0 else False + except: + cls.interactive = False + if cls.interactive and resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0: + # give a heads up if this is actually useless + critical("WARNING: core size limit is set 0, core files will NOT " + "be created") cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') - cls.vpp_api_test_bin = os.getenv("VPP_TEST_API_TEST_BIN", - "vpp-api-test") - cls.vpp_cmdline = [cls.vpp_bin, "unix", "nodaemon", "api-segment", "{", - "prefix", "unittest", "}"] + cls.vpp_cmdline = [cls.vpp_bin, "unix", "nodaemon", + "api-segment", "{", "prefix", cls.shm_prefix, "}"] if cls.plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path]) + info("vpp_cmdline: %s" % cls.vpp_cmdline) - cls.vpp_api_test_cmdline = [cls.vpp_api_test_bin, "chroot", "prefix", - "unittest"] - try: - cls.verbose = int(os.getenv("V", 0)) - except: - cls.verbose = 0 - - ## @var vpp_bin - # String variable to store the path to vpp (vector packet processor). - ## @var vpp_api_test_bin - # String variable to store the path to vpp_api_test (vpp API test tool). - ## @var vpp_cmdline - # List of command line attributes for vpp. - ## @var vpp_api_test_cmdline - # List of command line attributes for vpp_api_test. - ## @var verbose - # Integer variable to store required verbosity level. - - ## Class method to start the test case. - # 1. Initiate test case constants and set test case variables to default - # values. - # 2. Remove files from the shared memory. - # 3. Start vpp as a subprocess. - # @param cls The class pointer. @classmethod def setUpClass(cls): + """ + Perform class setup before running the testcase + Remove shared memory files, start vpp and connect the vpp-api + """ + cls.tempdir = tempfile.mkdtemp( + prefix='vpp-unittest-' + cls.__name__ + '-') + cls.shm_prefix = cls.tempdir.split("/")[-1] + os.chdir(cls.tempdir) + info("Temporary dir is %s, shm prefix is %s", + cls.tempdir, cls.shm_prefix) cls.setUpConstants() cls.pg_streams = [] - cls.MY_MACS = {} - cls.MY_IP4S = {} - cls.MY_IP6S = {} - cls.VPP_MACS = {} - cls.VPP_IP4S = {} - cls.VPP_IP6S = {} cls.packet_infos = {} - print "==================================================================" - print YELLOW + getdoc(cls) + END - print "==================================================================" - os.system("rm -f /dev/shm/unittest-global_vm") - os.system("rm -f /dev/shm/unittest-vpe-api") - os.system("rm -f /dev/shm/unittest-db") - cls.vpp = subprocess.Popen(cls.vpp_cmdline, stderr=subprocess.PIPE) - ## @var pg_streams - # List variable to store packet-generator streams for interfaces. - ## @var MY_MACS - # Dictionary variable to store host MAC addresses connected to packet - # generator interfaces. - ## @var MY_IP4S - # Dictionary variable to store host IPv4 addresses connected to packet - # generator interfaces. - ## @var MY_IP6S - # Dictionary variable to store host IPv6 addresses connected to packet - # generator interfaces. - ## @var VPP_MACS - # Dictionary variable to store VPP MAC addresses of the packet - # generator interfaces. - ## @var VPP_IP4S - # Dictionary variable to store VPP IPv4 addresses of the packet - # generator interfaces. - ## @var VPP_IP6S - # Dictionary variable to store VPP IPv6 addresses of the packet - # generator interfaces. - ## @var vpp - # Test case object variable to store file descriptor of running vpp - # subprocess with open pipe to the standard error stream per - # VppTestCase object. - - ## Class method to do cleaning when all tests (test_) defined for - # VppTestCase class are finished. - # 1. Terminate vpp and kill all vpp instances. - # 2. Remove files from the shared memory. - # @param cls The class pointer. + cls.verbose = 0 + print(double_line_delim) + print(YELLOW + getdoc(cls) + COLOR_RESET) + print(double_line_delim) + # need to catch exceptions here because if we raise, then the cleanup + # doesn't get called and we might end with a zombie vpp + try: + cls.vpp = subprocess.Popen(cls.vpp_cmdline, stderr=subprocess.PIPE) + debug("Spawned VPP with PID: %d" % cls.vpp.pid) + cls.vpp_dead = False + cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix) + cls.vapi.register_hook(PollHook(cls)) + cls.vapi.connect() + except: + cls.vpp.terminate() + del cls.vpp + @classmethod def quit(cls): - cls.vpp.terminate() - cls.vpp = None - os.system("rm -f /dev/shm/unittest-global_vm") - os.system("rm -f /dev/shm/unittest-vpe-api") - os.system("rm -f /dev/shm/unittest-db") - - ## Class method to define tear down action of the VppTestCase class. - # @param cls The class pointer. + """ + Disconnect vpp-api, kill vpp and cleanup shared memory files + """ + if hasattr(cls, 'vpp'): + cls.vapi.disconnect() + cls.vpp.poll() + if cls.vpp.returncode is None: + cls.vpp.terminate() + del cls.vpp + @classmethod def tearDownClass(cls): + """ Perform final cleanup after running all tests in this test-case """ cls.quit() - ## Method to define tear down VPP actions of the test case. - # @param self The object pointer. def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show hardware") - self.cli(2, "show ip arp") - self.cli(2, "show ip fib") - self.cli(2, "show error") - self.cli(2, "show run") - - ## Method to define setup action of the test case. - # @param self The object pointer. + """ Show various debug prints after each test """ + if not self.vpp_dead: + info(self.vapi.cli("show int")) + info(self.vapi.cli("show trace")) + info(self.vapi.cli("show hardware")) + info(self.vapi.cli("show error")) + info(self.vapi.cli("show run")) + def setUp(self): - self.cli(2, "clear trace") + """ Clear trace before running each test""" + self.vapi.cli("clear trace") + # store the test instance inside the test class - so that objects + # holding the class can access instance methods (like assertEqual) + type(self).test_instance = self - ## Class method to print logs. - # Based on set level of verbosity print text in the terminal. - # @param cls The class pointer. - # @param s String variable to store text to be printed. - # @param v Integer variable to store required level of verbosity. - @classmethod - def log(cls, s, v=1): - if cls.verbose >= v: - print "LOG: " + LPURPLE + s + END - - ## Class method to execute api commands. - # Based on set level of verbosity print the output of the api command in - # the terminal. - # @param cls The class pointer. - # @param s String variable to store api command string. - @classmethod - def api(cls, s): - p = subprocess.Popen(cls.vpp_api_test_cmdline, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) - if cls.verbose > 0: - print "API: " + RED + s + END - p.stdin.write(s) - out = p.communicate()[0] - out = out.replace("vat# ", "", 2) - if cls.verbose > 0: - if len(out) > 1: - print YELLOW + out + END - ## @var p - # Object variable to store file descriptor of vpp_api_test subprocess - # with open pipes to the standard output, inputs and error streams. - ## @var out - # Tuple variable to store standard output of vpp_api_test subprocess - # where the string "vat# " is replaced by empty string later. - - ## Class method to execute cli commands. - # Based on set level of verbosity of the log and verbosity defined by - # environmental variable execute the cli command and print the output in - # the terminal. - # CLI command is executed via vpp API test tool (exec + cli_command) - # @param cls The class pointer. - # @param v Integer variable to store required level of verbosity. - # @param s String variable to store cli command string. - @classmethod - def cli(cls, v, s): - if cls.verbose < v: - return - p = subprocess.Popen(cls.vpp_api_test_cmdline, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE) - if cls.verbose > 0: - print "CLI: " + RED + s + END - p.stdin.write('exec ' + s) - out = p.communicate()[0] - out = out.replace("vat# ", "", 2) - if cls.verbose > 0: - if len(out) > 1: - print YELLOW + out + END - ## @var p - # Object variable to store file descriptor of vpp_api_test subprocess - # with open pipes to the standard output, inputs and error streams. - ## @var out - # Tuple variable to store standard output of vpp_api_test subprocess - # where the string "vat# " is replaced by empty string later. - - ## Class method to create incoming packet stream for the packet-generator - # interface. - # Delete old /tmp/pgX_in.pcap file if exists and create the empty one and - # fill it with provided packets and add it to pg_streams list. - # @param cls The class pointer. - # @param i Integer variable to store the index of the packet-generator - # interface to create packet stream for. - # @param pkts List variable to store packets to be added to the stream. @classmethod - def pg_add_stream(cls, i, pkts): - os.system("rm -f /tmp/pg%u_in.pcap" % i) - wrpcap("/tmp/pg%u_in.pcap" % i, pkts) - # no equivalent API command - cls.cli(0, "packet-generator new pcap /tmp/pg%u_in.pcap source pg%u" - " name pcap%u" % (i, i, i)) - cls.pg_streams.append('pcap%u' % i) - - ## Class method to enable packet capturing for the packet-generator - # interface. - # Delete old /tmp/pgX_out.pcap file if exists and set the packet-generator - # to capture outgoing packets to /tmp/pgX_out.pcap file. - # @param cls The class pointer. - # @param args List variable to store the indexes of the packet-generator - # interfaces to start packet capturing for. - @classmethod - def pg_enable_capture(cls, args): - for i in args: - os.system("rm -f /tmp/pg%u_out.pcap" % i) - cls.cli(0, "packet-generator capture pg%u pcap /tmp/pg%u_out.pcap" - % (i, i)) - - ## Class method to start packet sending. - # Start to send packets for all defined pg streams. Delete every stream - # from the stream list when sent and clear the pg_streams list. - # @param cls The class pointer. + def pg_enable_capture(cls, interfaces): + """ + Enable capture on packet-generator interfaces + + :param interfaces: iterable interface indexes + + """ + for i in interfaces: + i.enable_capture() + @classmethod def pg_start(cls): - cls.cli(2, "trace add pg-input 50") # 50 is maximum - cls.cli(0, 'packet-generator enable') + """ + Enable the packet-generator and send all prepared packet streams + Remove the packet streams afterwards + """ + cls.vapi.cli("trace add pg-input 50") # 50 is maximum + cls.vapi.cli('packet-generator enable') + sleep(1) # give VPP some time to process the packets for stream in cls.pg_streams: - cls.cli(0, 'packet-generator delete %s' % stream) + cls.vapi.cli('packet-generator delete %s' % stream) cls.pg_streams = [] - ## Class method to return captured packets. - # Return packet captured for the defined packet-generator interface. Open - # the corresponding pcap file (/tmp/pgX_out.pcap), read the content and - # store captured packets to output variable. - # @param cls The class pointer. - # @param o Integer variable to store the index of the packet-generator - # interface. - # @return output List of packets captured on the defined packet-generator - # interface. If the corresponding pcap file (/tmp/pgX_out.pcap) does not - # exist return empty list. - @classmethod - def pg_get_capture(cls, o): - pcap_filename = "/tmp/pg%u_out.pcap" % o - try: - output = rdpcap(pcap_filename) - except IOError: # TODO - cls.log("WARNING: File %s does not exist, probably because no" - " packets arrived" % pcap_filename) - return [] - return output - ## @var pcap_filename - # File descriptor to the corresponding pcap file. - - ## Class method to create packet-generator interfaces. - # Create packet-generator interfaces and add host MAC addresses connected - # to these packet-generator interfaces to the MY_MACS dictionary. - # @param cls The class pointer. - # @param args List variable to store the indexes of the packet-generator - # interfaces to be created. @classmethod - def create_interfaces(cls, args): - for i in args: - cls.MY_MACS[i] = "02:00:00:00:ff:%02x" % i - cls.log("My MAC address is %s" % (cls.MY_MACS[i])) - cls.api("pg_create_interface if_id %u" % i) - cls.api("sw_interface_set_flags pg%u admin-up" % i) - - ## Static method to extend packet to specified size - # Extend provided packet to the specified size (including Ethernet FCS). - # The packet is extended by adding corresponding number of spaces to the - # packet payload. - # NOTE: Currently works only when Raw layer is present. - # @param packet Variable to store packet object. - # @param size Integer variable to store the required size of the packet. + def create_pg_interfaces(cls, interfaces): + """ + Create packet-generator interfaces + + :param interfaces: iterable indexes of the interfaces + + """ + result = [] + for i in interfaces: + intf = VppPGInterface(cls, i) + setattr(cls, intf.name, intf) + result.append(intf) + cls.pg_interfaces = result + return result + @staticmethod def extend_packet(packet, size): + """ + Extend packet to given size by padding with spaces + NOTE: Currently works only when Raw layer is present. + + :param packet: packet + :param size: target size + + """ packet_len = len(packet) + 4 extend = size - packet_len if extend > 0: packet[Raw].load += ' ' * extend - ## @var packet_len - # Integer variable to store the current packet length including - # Ethernet FCS. - ## @var extend - # Integer variable to store the size of the packet extension. - - ## Method to add packet info object to the packet_infos list. - # Extend the existing packet_infos list with the given information from - # the packet. - # @param self The object pointer. - # @param info Object to store required information from the packet. + def add_packet_info_to_list(self, info): + """ + Add packet info to the testcase's packet info list + + :param info: packet info + + """ info.index = len(self.packet_infos) self.packet_infos[info.index] = info - ## @var info.index - # Info object attribute to store the packet order in the stream. - ## @var packet_infos - # List variable to store required information from packets. - - ## Method to create packet info object. - # Create the existing packet_infos list with the given information from - # the packet. - # @param self The object pointer. - # @param pg_id Integer variable to store the index of the packet-generator - # interface. - def create_packet_info(self, pg_id, target_id): + + def create_packet_info(self, src_pg_index, dst_pg_index): + """ + Create packet info object containing the source and destination indexes + and add it to the testcase's packet info list + + :param src_pg_index: source packet-generator index + :param dst_pg_index: destination packet-generator index + + :returns: _PacketInfo object + + """ info = _PacketInfo() self.add_packet_info_to_list(info) - info.src = pg_id - info.dst = target_id + info.src = src_pg_index + info.dst = dst_pg_index return info - ## @var info - # Object to store required information from packet. - ## @var info.src - # Info object attribute to store the index of the source packet - # generator interface of the packet. - ## @var info.dst - # Info object attribute to store the index of the destination packet - # generator interface of the packet. - - ## Static method to return packet info string. - # Create packet info string from the provided info object that will be put - # to the packet payload. - # @param info Object to store required information from the packet. - # @return String of information about packet's order in the stream, source - # and destination packet generator interface. + @staticmethod def info_to_payload(info): + """ + Convert _PacketInfo object to packet payload + + :param info: _PacketInfo object + + :returns: string containing serialized data from packet info + """ return "%d %d %d" % (info.index, info.src, info.dst) - ## Static method to create packet info object from the packet payload. - # Create packet info object and set its attribute values based on data - # gained from the packet payload. - # @param payload String variable to store packet payload. - # @return info Object to store required information about the packet. @staticmethod def payload_to_info(payload): + """ + Convert packet payload to _PacketInfo object + + :param payload: packet payload + + :returns: _PacketInfo object containing de-serialized data from payload + + """ numbers = payload.split() info = _PacketInfo() info.index = int(numbers[0]) info.src = int(numbers[1]) info.dst = int(numbers[2]) return info - ## @var info.index - # Info object attribute to store the packet order in the stream. - ## @var info.src - # Info object attribute to store the index of the source packet - # generator interface of the packet. - ## @var info.dst - # Info object attribute to store the index of the destination packet - # generator interface of the packet. - - ## Method to return packet info object of the next packet in - # the packet_infos list. - # Get the next packet info object from the packet_infos list by increasing - # the packet_infos list index by one. - # @param self The object pointer. - # @param info Object to store required information about the packet. - # @return packet_infos[next_index] Next info object from the packet_infos - # list with stored information about packets. Return None if the end of - # the list is reached. + def get_next_packet_info(self, info): + """ + Iterate over the packet info list stored in the testcase + Start iteration with first element if info is None + Continue based on index in info if info is specified + + :param info: info or None + :returns: next info in list or None if no more infos + """ if info is None: next_index = 0 else: @@ -427,208 +318,208 @@ class VppTestCase(unittest.TestCase): return None else: return self.packet_infos[next_index] - ## @var next_index - # Integer variable to store the index of the next info object. - - ## Method to return packet info object of the next packet with the required - # source packet generator interface. - # Iterate over the packet_infos list and search for the next packet info - # object with the required source packet generator interface. - # @param self The object pointer. - # @param src_pg Integer variable to store index of requested source packet - # generator interface. - # @param info Object to store required information about the packet. - # @return packet_infos[next_index] Next info object from the packet_infos - # list with stored information about packets. Return None if the end of - # the list is reached. - def get_next_packet_info_for_interface(self, src_pg, info): + + def get_next_packet_info_for_interface(self, src_index, info): + """ + Search the packet info list for the next packet info with same source + interface index + + :param src_index: source interface index to search for + :param info: packet info - where to start the search + :returns: packet info or None + + """ while True: info = self.get_next_packet_info(info) if info is None: return None - if info.src == src_pg: + if info.src == src_index: return info - ## @var info.src - # Info object attribute to store the index of the source packet - # generator interface of the packet. - - ## Method to return packet info object of the next packet with required - # source and destination packet generator interfaces. - # Search for the next packet info object with the required source and - # destination packet generator interfaces. - # @param self The object pointer. - # @param src_pg Integer variable to store the index of the requested source - # packet generator interface. - # @param dst_pg Integer variable to store the index of the requested source - # packet generator interface. - # @param info Object to store required information about the packet. - # @return info Object with the info about the next packet with with - # required source and destination packet generator interfaces. Return None - # if there is no other packet with required data. - def get_next_packet_info_for_interface2(self, src_pg, dst_pg, info): + + def get_next_packet_info_for_interface2(self, src_index, dst_index, info): + """ + Search the packet info list for the next packet info with same source + and destination interface indexes + + :param src_index: source interface index to search for + :param dst_index: destination interface index to search for + :param info: packet info - where to start the search + :returns: packet info or None + + """ while True: - info = self.get_next_packet_info_for_interface(src_pg, info) + info = self.get_next_packet_info_for_interface(src_index, info) if info is None: return None - if info.dst == dst_pg: + if info.dst == dst_index: return info - ## @var info.dst - # Info object attribute to store the index of the destination packet - # generator interface of the packet. -## Subclass of the python unittest.TestResult class. -# -# This subclass provides methods to compile information about which tests have -# succeeded and which have failed. class VppTestResult(unittest.TestResult): - ## The constructor. - # @param stream File descriptor to store where to report test results. Set - # to the standard error stream by default. - # @param descriptions Boolean variable to store information if to use test - # case descriptions. - # @param verbosity Integer variable to store required verbosity level. + """ + @property result_string + String variable to store the test case result string. + @property errors + List variable containing 2-tuples of TestCase instances and strings + holding formatted tracebacks. Each tuple represents a test which + raised an unexpected exception. + @property failures + List variable containing 2-tuples of TestCase instances and strings + holding formatted tracebacks. Each tuple represents a test where + a failure was explicitly signalled using the TestCase.assert*() + methods. + """ + def __init__(self, stream, descriptions, verbosity): + """ + :param stream File descriptor to store where to report test results. Set + to the standard error stream by default. + :param descriptions Boolean variable to store information if to use test + case descriptions. + :param verbosity Integer variable to store required verbosity level. + """ unittest.TestResult.__init__(self, stream, descriptions, verbosity) self.stream = stream self.descriptions = descriptions self.verbosity = verbosity self.result_string = None - ## @var result_string - # String variable to store the test case result string. - - ## Method called when the test case succeeds. - # Run the default implementation (that does nothing) and set the result - # string in case of test case success. - # @param self The object pointer. - # @param test Object variable to store the test case instance. def addSuccess(self, test): + """ + Record a test succeeded result + + :param test: + + """ unittest.TestResult.addSuccess(self, test) - self.result_string = GREEN + "OK" + END - ## @var result_string - # String variable to store the test case result string. - - ## Method called when the test case signals a failure. - # Run the default implementation that appends a tuple (test, formatted_err) - # to the instance's failures attribute, where formatted_err is a formatted - # traceback derived from err and set the result string in case of test case - # success. - # @param self The object pointer. - # @param test Object variable to store the test case instance. - # @param err Tuple variable to store the error data: - # (type, value, traceback). + self.result_string = GREEN + "OK" + COLOR_RESET + + def addSkip(self, test, reason): + """ + Record a test skipped. + + :param test: + :param reason: + + """ + unittest.TestResult.addSkip(self, test, reason) + self.result_string = YELLOW + "SKIP" + COLOR_RESET + def addFailure(self, test, err): + """ + Record a test failed result + + :param test: + :param err: error message + + """ unittest.TestResult.addFailure(self, test, err) - self.result_string = RED + "FAIL" + END - ## @var result_string - # String variable to store the test case result string. - - ## Method called when the test case raises an unexpected exception. - # Run the default implementation that appends a tuple (test, formatted_err) - # to the instance's error attribute, where formatted_err is a formatted - # traceback derived from err and set the result string in case of test case - # unexpected failure. - # @param self The object pointer. - # @param test Object variable to store the test case instance. - # @param err Tuple variable to store the error data: - # (type, value, traceback). + if hasattr(test, 'tempdir'): + self.result_string = RED + "FAIL" + COLOR_RESET + \ + ' [ temp dir used by test case: ' + test.tempdir + ' ]' + else: + self.result_string = RED + "FAIL" + COLOR_RESET + ' [no temp dir]' + def addError(self, test, err): + """ + Record a test error result + + :param test: + :param err: error message + + """ unittest.TestResult.addError(self, test, err) - self.result_string = RED + "ERROR" + END - ## @var result_string - # String variable to store the test case result string. - - ## Method to get the description of the test case. - # Used to get the description string from the test case object. - # @param self The object pointer. - # @param test Object variable to store the test case instance. - # @return String of the short description if exist otherwise return test - # case name string. + if hasattr(test, 'tempdir'): + self.result_string = RED + "ERROR" + COLOR_RESET + \ + ' [ temp dir used by test case: ' + test.tempdir + ' ]' + else: + self.result_string = RED + "ERROR" + COLOR_RESET + ' [no temp dir]' + def getDescription(self, test): + """ + Get test description + + :param test: + :returns: test description + + """ # TODO: if none print warning not raise exception short_description = test.shortDescription() if self.descriptions and short_description: return short_description else: return str(test) - ## @var short_description - # String variable to store the short description of the test case. - - ## Method called when the test case is about to be run. - # Run the default implementation and based on the set verbosity level write - # the starting string to the output stream. - # @param self The object pointer. - # @param test Object variable to store the test case instance. + def startTest(self, test): + """ + Start a test + + :param test: + + """ unittest.TestResult.startTest(self, test) if self.verbosity > 0: - self.stream.writeln("Starting " + self.getDescription(test) + " ...") - self.stream.writeln("------------------------------------------------------------------") - - ## Method called after the test case has been executed. - # Run the default implementation and based on the set verbosity level write - # the result string to the output stream. - # @param self The object pointer. - # @param test Object variable to store the test case instance. + self.stream.writeln( + "Starting " + self.getDescription(test) + " ...") + self.stream.writeln(single_line_delim) + def stopTest(self, test): + """ + Stop a test + + :param test: + + """ unittest.TestResult.stopTest(self, test) if self.verbosity > 0: - self.stream.writeln("------------------------------------------------------------------") - self.stream.writeln("%-60s%s" % (self.getDescription(test), self.result_string)) - self.stream.writeln("------------------------------------------------------------------") + self.stream.writeln(single_line_delim) + self.stream.writeln("%-60s%s" % + (self.getDescription(test), self.result_string)) + self.stream.writeln(single_line_delim) else: - self.stream.writeln("%-60s%s" % (self.getDescription(test), self.result_string)) + self.stream.writeln("%-60s%s" % + (self.getDescription(test), self.result_string)) - ## Method to write errors and failures information to the output stream. - # Write content of errors and failures lists to the output stream. - # @param self The object pointer. def printErrors(self): + """ + Print errors from running the test case + """ self.stream.writeln() self.printErrorList('ERROR', self.errors) self.printErrorList('FAIL', self.failures) - ## @var errors - # List variable containing 2-tuples of TestCase instances and strings - # holding formatted tracebacks. Each tuple represents a test which - # raised an unexpected exception. - ## @var failures - # List variable containing 2-tuples of TestCase instances and strings - # holding formatted tracebacks. Each tuple represents a test where - # a failure was explicitly signalled using the TestCase.assert*() - # methods. - - ## Method to write the error information to the output stream. - # Write content of error lists to the output stream together with error - # type and test case description. - # @param self The object pointer. - # @param flavour String variable to store error type. - # @param errors List variable to store 2-tuples of TestCase instances and - # strings holding formatted tracebacks. + def printErrorList(self, flavour, errors): + """ + Print error list to the output stream together with error type + and test case description. + + :param flavour: error type + :param errors: iterable errors + + """ for test, err in errors: - self.stream.writeln('=' * 70) - self.stream.writeln("%s: %s" % (flavour, self.getDescription(test))) - self.stream.writeln('-' * 70) + self.stream.writeln(double_line_delim) + self.stream.writeln("%s: %s" % + (flavour, self.getDescription(test))) + self.stream.writeln(single_line_delim) self.stream.writeln("%s" % err) - ## @var test - # Object variable to store the test case instance. - ## @var err - # String variable to store formatted tracebacks. -## Subclass of the python unittest.TextTestRunner class. -# -# A basic test runner implementation which prints results on standard error. class VppTestRunner(unittest.TextTestRunner): - ## Class object variable to store the results of a set of tests. - resultclass = VppTestResult - - ## Method to run the test. - # Print debug message in the terminal and run the standard run() method - # of the test runner collecting the result into the test result object. - # @param self The object pointer. - # @param test Object variable to store the test case instance. - # @return Test result object of the VppTestRunner. + """ + A basic test runner implementation which prints results on standard error. + """ + @property + def resultclass(self): + """Class maintaining the results of the tests""" + return VppTestResult + def run(self, test): - print "Running tests using custom test runner" # debug message + """ + Run the tests + + :param test: + + """ + print("Running tests using custom test runner") # debug message return super(VppTestRunner, self).run(test) diff --git a/test/hook.py b/test/hook.py new file mode 100644 index 00000000..9489aa9d --- /dev/null +++ b/test/hook.py @@ -0,0 +1,128 @@ +import signal +import os +import pexpect +from logging import * + + +class Hook(object): + """ + Generic hooks before/after API/CLI calls + """ + + def before_api(self, api_name, api_args): + """ + Function called before API call + Emit a debug message describing the API name and arguments + + @param api_name: name of the API + @param api_args: tuple containing the API arguments + """ + debug("API: %s (%s)" % (api_name, api_args)) + + def after_api(self, api_name, api_args): + """ + Function called after API call + + @param api_name: name of the API + @param api_args: tuple containing the API arguments + """ + pass + + def before_cli(self, cli): + """ + Function called before CLI call + Emit a debug message describing the CLI + + @param cli: CLI string + """ + debug("CLI: %s" % (cli)) + + def after_cli(self, cli): + """ + Function called after CLI call + """ + pass + + +class VppDiedError(Exception): + pass + + +class PollHook(Hook): + """ Hook which checks if the vpp subprocess is alive """ + + def __init__(self, testcase): + self.vpp_dead = False + self.testcase = testcase + + def spawn_gdb(self, gdb_path, core_path): + gdb_cmdline = gdb_path + ' ' + self.testcase.vpp_bin + ' ' + core_path + gdb = pexpect.spawn(gdb_cmdline) + gdb.interact() + try: + gdb.terminate(True) + except: + pass + if gdb.isalive(): + raise Exception("GDB refused to die...") + + def on_crash(self, core_path): + if self.testcase.interactive: + gdb_path = '/usr/bin/gdb' + if os.path.isfile(gdb_path) and os.access(gdb_path, os.X_OK): + # automatically attach gdb + self.spawn_gdb(gdb_path, core_path) + return + else: + error("Debugger '%s' does not exist or is not an executable.." % + gdb_path) + + critical('core file present, debug with: gdb ' + + self.testcase.vpp_bin + ' ' + core_path) + + def poll_vpp(self): + """ + Poll the vpp status and throw an exception if it's not running + :raises VppDiedError: exception if VPP is not running anymore + """ + if self.vpp_dead: + # already dead, nothing to do + return + + self.testcase.vpp.poll() + if self.testcase.vpp.returncode is not None: + signaldict = dict( + (k, v) for v, k in reversed(sorted(signal.__dict__.items())) + if v.startswith('SIG') and not v.startswith('SIG_')) + msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( + self.testcase.vpp.returncode, + signaldict[abs(self.testcase.vpp.returncode)]) + critical(msg) + core_path = self.testcase.tempdir + '/core' + if os.path.isfile(core_path): + self.on_crash(core_path) + self.testcase.vpp_dead = True + raise VppDiedError(msg) + + def after_api(self, api_name, api_args): + """ + Check if VPP died after executing an API + + :param api_name: name of the API + :param api_args: tuple containing the API arguments + :raises VppDiedError: exception if VPP is not running anymore + + """ + super(PollHook, self).after_api(api_name, api_args) + self.poll_vpp() + + def after_cli(self, cli): + """ + Check if VPP died after executing a CLI + + :param cli: CLI string + :raises Exception: exception if VPP is not running anymore + + """ + super(PollHook, self).after_cli(cli) + self.poll_vpp() diff --git a/test/template_bd.py b/test/template_bd.py index 473c4228..6c6fb3da 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -7,100 +7,94 @@ from scapy.layers.inet import IP, UDP class BridgeDomain(object): - def __init__(self): - ## Ethernet frame which is send to pg0 interface and is forwarded to pg1 - self.payload_0_1 = ( - Ether(src='00:00:00:00:00:01', dst='00:00:00:00:00:02') / - IP(src='1.2.3.4', dst='4.3.2.1') / - UDP(sport=10000, dport=20000) / - Raw('\xa5' * 100)) - - ## Ethernet frame which is send to pg1 interface and is forwarded to pg0 - self.payload_1_0 = ( - Ether(src='00:00:00:00:00:02', dst='00:00:00:00:00:01') / - IP(src='4.3.2.1', dst='1.2.3.4') / - UDP(sport=20000, dport=10000) / - Raw('\xa5' * 100)) - - ## Test case must implement this method, so template known how to send - # encapsulated frame. + """ Bridge domain abstraction """ + + @property + def frame_pg0_to_pg1(self): + """ Ethernet frame sent from pg0 and expected to arrive at pg1 """ + return (Ether(src='00:00:00:00:00:01', dst='00:00:00:00:00:02') / + IP(src='1.2.3.4', dst='4.3.2.1') / + UDP(sport=10000, dport=20000) / + Raw('\xa5' * 100)) + + @property + def frame_pg1_to_pg0(self): + """ Ethernet frame sent from pg1 and expected to arrive at pg0 """ + return (Ether(src='00:00:00:00:00:02', dst='00:00:00:00:00:01') / + IP(src='4.3.2.1', dst='1.2.3.4') / + UDP(sport=20000, dport=10000) / + Raw('\xa5' * 100)) + @abstractmethod def encapsulate(self, pkt): + """ Encapsulate packet """ pass - ## Test case must implement this method, so template known how to get - # original payload. @abstractmethod def decapsulate(self, pkt): + """ Decapsulate packet """ pass - ## Test case must implement this method, so template known how if the - # received frame is corectly encapsulated. @abstractmethod def check_encapsulation(self, pkt): + """ Verify the encapsulation """ pass - ## On pg0 interface are encapsulated frames, on pg1 are testing frames - # without encapsulation def test_decap(self): - ## Prepare Ethernet frame that will be send encapsulated. - pkt_to_send = self.encapsulate(self.payload_0_1) + """ Decapsulation test + Send encapsulated frames from pg0 + Verify receipt of decapsulated frames on pg1 + """ + + encapsulated_pkt = self.encapsulate(self.frame_pg0_to_pg1) - ## Add packet to list of packets. - self.pg_add_stream(0, [pkt_to_send, ]) + self.pg0.add_stream([encapsulated_pkt, ]) - ## Enable Packet Capture on both ports. - self.pg_enable_capture([0, 1]) + self.pg1.enable_capture() - ## Start all streams self.pg_start() - ## Pick first received frame and check if is same as non-encapsulated - # frame. - out = self.pg_get_capture(1) + # Pick first received frame and check if it's the non-encapsulated frame + out = self.pg1.get_capture() self.assertEqual(len(out), 1, 'Invalid number of packets on ' 'output: {}'.format(len(out))) pkt = out[0] # TODO: add error messages - self.assertEqual(pkt[Ether].src, self.payload_0_1[Ether].src) - self.assertEqual(pkt[Ether].dst, self.payload_0_1[Ether].dst) - self.assertEqual(pkt[IP].src, self.payload_0_1[IP].src) - self.assertEqual(pkt[IP].dst, self.payload_0_1[IP].dst) - self.assertEqual(pkt[UDP].sport, self.payload_0_1[UDP].sport) - self.assertEqual(pkt[UDP].dport, self.payload_0_1[UDP].dport) - self.assertEqual(pkt[Raw], self.payload_0_1[Raw]) - - ## Send non-encapsulated Ethernet frame from pg1 interface and expect - # encapsulated frame on pg0. On pg0 interface are encapsulated frames, - # on pg1 are testing frames without encapsulation. + self.assertEqual(pkt[Ether].src, self.frame_pg0_to_pg1[Ether].src) + self.assertEqual(pkt[Ether].dst, self.frame_pg0_to_pg1[Ether].dst) + self.assertEqual(pkt[IP].src, self.frame_pg0_to_pg1[IP].src) + self.assertEqual(pkt[IP].dst, self.frame_pg0_to_pg1[IP].dst) + self.assertEqual(pkt[UDP].sport, self.frame_pg0_to_pg1[UDP].sport) + self.assertEqual(pkt[UDP].dport, self.frame_pg0_to_pg1[UDP].dport) + self.assertEqual(pkt[Raw], self.frame_pg0_to_pg1[Raw]) + def test_encap(self): - ## Create packet generator stream. - self.pg_add_stream(1, [self.payload_1_0]) + """ Encapsulation test + Send frames from pg1 + Verify receipt of encapsulated frames on pg0 + """ + self.pg1.add_stream([self.frame_pg1_to_pg0]) - ## Enable Packet Capture on both ports. - self.pg_enable_capture([0, 1]) + self.pg0.enable_capture() - ## Start all streams. self.pg_start() - ## Pick first received frame and check if is corectly encapsulated. - out = self.pg_get_capture(0) + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture() self.assertEqual(len(out), 1, 'Invalid number of packets on ' 'output: {}'.format(len(out))) - rcvd = out[0] - self.check_encapsulation(rcvd) + pkt = out[0] + self.check_encapsulation(pkt) - ## Get original frame from received packet and check if is same as - # sended frame. - rcvd_payload = self.decapsulate(rcvd) + payload = self.decapsulate(pkt) # TODO: add error messages - self.assertEqual(rcvd_payload[Ether].src, self.payload_1_0[Ether].src) - self.assertEqual(rcvd_payload[Ether].dst, self.payload_1_0[Ether].dst) - self.assertEqual(rcvd_payload[IP].src, self.payload_1_0[IP].src) - self.assertEqual(rcvd_payload[IP].dst, self.payload_1_0[IP].dst) - self.assertEqual(rcvd_payload[UDP].sport, self.payload_1_0[UDP].sport) - self.assertEqual(rcvd_payload[UDP].dport, self.payload_1_0[UDP].dport) - self.assertEqual(rcvd_payload[Raw], self.payload_1_0[Raw]) + self.assertEqual(payload[Ether].src, self.frame_pg1_to_pg0[Ether].src) + self.assertEqual(payload[Ether].dst, self.frame_pg1_to_pg0[Ether].dst) + self.assertEqual(payload[IP].src, self.frame_pg1_to_pg0[IP].src) + self.assertEqual(payload[IP].dst, self.frame_pg1_to_pg0[IP].dst) + self.assertEqual(payload[UDP].sport, self.frame_pg1_to_pg0[UDP].sport) + self.assertEqual(payload[UDP].dport, self.frame_pg1_to_pg0[UDP].dport) + self.assertEqual(payload[Raw], self.frame_pg1_to_pg0[Raw]) diff --git a/test/test_ip.py b/test/test_ip.py index 3a2c9011..48155a5a 100644 --- a/test/test_ip.py +++ b/test/test_ip.py @@ -1,230 +1,125 @@ #!/usr/bin/env python -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) - import unittest +import socket +from logging import * + from framework import VppTestCase, VppTestRunner -from util import Util +from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint from scapy.packet import Raw -from scapy.layers.l2 import Ether, ARP, Dot1Q +from scapy.layers.l2 import Ether, Dot1Q, ARP from scapy.layers.inet import IP, UDP -class TestIPv4(Util, VppTestCase): +class TestIPv4(VppTestCase): """ IPv4 Test Case """ @classmethod def setUpClass(cls): super(TestIPv4, cls).setUpClass() - try: - cls.create_interfaces_and_subinterfaces() - - # configure IPv4 on hardware interfaces - cls.config_ip4(cls.interfaces) - - cls.config_ip4_on_software_interfaces(cls.interfaces) - - # resolve ARP using hardware interfaces - cls.resolve_arp(cls.interfaces) - - # let VPP know MAC addresses of peer (sub)interfaces - cls.resolve_arp_on_software_interfaces(cls.interfaces) - - # config 2M FIB enries - cls.config_fib_entries(2000000) - - except Exception as e: - super(TestIPv4, cls).tearDownClass() - raise - - def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show hardware") - self.cli(2, "show ip arp") - # self.cli(2, "show ip fib") # 2M entries - self.cli(2, "show error") - self.cli(2, "show run") - - @classmethod - def create_vlan_subif(cls, pg_index, vlan): - cls.api("create_vlan_subif pg%u vlan %u" % (pg_index, vlan)) - - @classmethod - def create_dot1ad_subif(cls, pg_index, sub_id, outer_vlan_id, inner_vlan_id): - cls.api("create_subif pg%u sub_id %u outer_vlan_id %u inner_vlan_id %u dot1ad" - % (pg_index, sub_id, outer_vlan_id, inner_vlan_id)) - - class SoftInt(object): - pass - - class HardInt(SoftInt): - pass - - class Subint(SoftInt): - def __init__(self, sub_id): - self.sub_id = sub_id - - class Dot1QSubint(Subint): - def __init__(self, sub_id, vlan=None): - if vlan is None: - vlan = sub_id - super(TestIPv4.Dot1QSubint, self).__init__(sub_id) - self.vlan = vlan - - class Dot1ADSubint(Subint): - def __init__(self, sub_id, outer_vlan, inner_vlan): - super(TestIPv4.Dot1ADSubint, self).__init__(sub_id) - self.outer_vlan = outer_vlan - self.inner_vlan = inner_vlan - - @classmethod - def create_interfaces_and_subinterfaces(cls): - cls.interfaces = range(3) - - cls.create_interfaces(cls.interfaces) + def setUp(self): + super(TestIPv4, self).setUp() - # Make vpp_api_test see interfaces created using debug CLI (in function create_interfaces) - cls.api("sw_interface_dump") + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) - cls.INT_DETAILS = dict() + # create 2 subinterfaces for pg1 and pg2 + self.sub_interfaces = [ + VppDot1QSubint(self, self.pg1, 100), + VppDot1ADSubint(self, self.pg2, 200, 300, 400)] - cls.INT_DETAILS[0] = cls.HardInt() + # packet flows mapping pg0 -> pg1.sub, pg2.sub, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1.sub_if, self.pg2.sub_if] + self.flows[self.pg1.sub_if] = [self.pg0, self.pg2.sub_if] + self.flows[self.pg2.sub_if] = [self.pg0, self.pg1.sub_if] - cls.INT_DETAILS[1] = cls.Dot1QSubint(100) - cls.create_vlan_subif(1, cls.INT_DETAILS[1].vlan) + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] + self.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] - # FIXME: Wrong packet format/wrong layer on output of interface 2 - #self.INT_DETAILS[2] = self.Dot1ADSubint(10, 200, 300) - #self.create_dot1ad_subif(2, self.INT_DETAILS[2].sub_id, self.INT_DETAILS[2].outer_vlan, self.INT_DETAILS[2].inner_vlan) + self.interfaces = list(self.pg_interfaces) + self.interfaces.extend(self.sub_interfaces) - # Use dor1q for now - cls.INT_DETAILS[2] = cls.Dot1QSubint(200) - cls.create_vlan_subif(2, cls.INT_DETAILS[2].vlan) - - for i in cls.interfaces: - det = cls.INT_DETAILS[i] - if isinstance(det, cls.Subint): - cls.api("sw_interface_set_flags pg%u.%u admin-up" % (i, det.sub_id)) - - # IP adresses on subinterfaces - MY_SOFT_IP4S = {} - VPP_SOFT_IP4S = {} - - @classmethod - def config_ip4_on_software_interfaces(cls, args): - for i in args: - cls.MY_SOFT_IP4S[i] = "172.17.%u.2" % i - cls.VPP_SOFT_IP4S[i] = "172.17.%u.1" % i - if isinstance(cls.INT_DETAILS[i], cls.Subint): - interface = "pg%u.%u" % (i, cls.INT_DETAILS[i].sub_id) - else: - interface = "pg%u" % i - cls.api("sw_interface_add_del_address %s %s/24" % (interface, cls.VPP_SOFT_IP4S[i])) - cls.log("My subinterface IPv4 address is %s" % (cls.MY_SOFT_IP4S[i])) - - # let VPP know MAC addresses of peer (sub)interfaces - @classmethod - def resolve_arp_on_software_interfaces(cls, args): - for i in args: - ip = cls.VPP_SOFT_IP4S[i] - cls.log("Sending ARP request for %s on port %u" % (ip, i)) - packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / - ARP(op=ARP.who_has, pdst=ip, psrc=cls.MY_SOFT_IP4S[i], hwsrc=cls.MY_MACS[i])) - - cls.add_dot1_layers(i, packet) - - cls.pg_add_stream(i, packet) - cls.pg_enable_capture([i]) - - cls.cli(2, "trace add pg-input 1") - cls.pg_start() - - # We don't need to read output - - @classmethod - def config_fib_entries(cls, count): - n_int = len(cls.interfaces) - for i in cls.interfaces: - cls.api("ip_add_del_route 10.0.0.1/32 via %s count %u" % (cls.VPP_SOFT_IP4S[i], count / n_int)) - - @classmethod - def add_dot1_layers(cls, i, packet): - assert(type(packet) is Ether) - payload = packet.payload - det = cls.INT_DETAILS[i] - if isinstance(det, cls.Dot1QSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=det.sub_id) / payload) - elif isinstance(det, cls.Dot1ADSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=det.outer_vlan) / Dot1Q(vlan=det.inner_vlan) / payload) - packet.type = 0x88A8 + # setup all interfaces + for i in self.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() - def remove_dot1_layers(self, i, packet): - self.assertEqual(type(packet), Ether) - payload = packet.payload - det = self.INT_DETAILS[i] - if isinstance(det, self.Dot1QSubint): - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].vlan) - payload = payload.payload - elif isinstance(det, self.Dot1ADSubint): # TODO: change 88A8 type - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].outer_vlan) - payload = payload.payload - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].inner_vlan) - payload = payload.payload - packet.remove_payload() - packet.add_payload(payload) + # config 2M FIB enries + self.config_fib_entries(200) - def create_stream(self, pg_id): - pg_targets = [None] * 3 - pg_targets[0] = [1, 2] - pg_targets[1] = [0, 2] - pg_targets[2] = [0, 1] + def tearDown(self): + super(TestIPv4, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show ip arp")) + # info(self.vapi.cli("show ip fib")) # many entries + + def config_fib_entries(self, count): + n_int = len(self.interfaces) + percent = 0 + counter = 0.0 + dest_addr = socket.inet_pton(socket.AF_INET, "10.0.0.1") + dest_addr_len = 32 + for i in self.interfaces: + next_hop_address = i.local_ip4n + for j in range(count / n_int): + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, next_hop_address) + counter = counter + 1 + if counter / count * 100 > percent: + info("Configure %d FIB entries .. %d%% done" % + (count, percent)) + percent = percent + 1 + + def create_stream(self, src_if, packet_sizes): pkts = [] for i in range(0, 257): - target_pg_id = pg_targets[pg_id][i % 2] - info = self.create_packet_info(pg_id, target_pg_id) + dst_if = self.flows[src_if][i % 2] + info = self.create_packet_info( + src_if.sw_if_index, dst_if.sw_if_index) payload = self.info_to_payload(info) - p = (Ether(dst=self.VPP_MACS[pg_id], src=self.MY_MACS[pg_id]) / - IP(src=self.MY_SOFT_IP4S[pg_id], dst=self.MY_SOFT_IP4S[target_pg_id]) / + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / UDP(sport=1234, dport=1234) / Raw(payload)) info.data = p.copy() - self.add_dot1_layers(pg_id, p) - if not isinstance(self.INT_DETAILS[pg_id], self.Subint): - packet_sizes = [64, 512, 1518, 9018] - else: - packet_sizes = [64, 512, 1518+4, 9018+4] - size = packet_sizes[(i / 2) % len(packet_sizes)] + if isinstance(src_if, VppSubInterface): + p = src_if.add_dot1_layer(p) + size = packet_sizes[(i // 2) % len(packet_sizes)] self.extend_packet(p, size) pkts.append(p) return pkts - def verify_capture(self, o, capture): - last_info = {} + def verify_capture(self, dst_if, capture): + info("Verifying capture on interface %s" % dst_if.name) + last_info = dict() for i in self.interfaces: - last_info[i] = None + last_info[i.sw_if_index] = None + is_sub_if = False + dst_sw_if_index = dst_if.sw_if_index + if hasattr(dst_if, 'parent'): + is_sub_if = True for packet in capture: - self.remove_dot1_layers(o, packet) # Check VLAN tags and Ethernet header + if is_sub_if: + # Check VLAN tags and Ethernet header + packet = dst_if.remove_dot1_layer(packet) self.assertTrue(Dot1Q not in packet) try: ip = packet[IP] udp = packet[UDP] payload_info = self.payload_to_info(str(packet[Raw])) packet_index = payload_info.index - src_pg = payload_info.src - dst_pg = payload_info.dst - self.assertEqual(dst_pg, o) - self.log("Got packet on port %u: src=%u (id=%u)" % (o, src_pg, packet_index), 2) - next_info = self.get_next_packet_info_for_interface2(src_pg, dst_pg, last_info[src_pg]) - last_info[src_pg] = next_info + self.assertEqual(payload_info.dst, dst_sw_if_index) + debug("Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info self.assertTrue(next_info is not None) self.assertEqual(packet_index, next_info.index) saved_packet = next_info.data @@ -234,28 +129,37 @@ class TestIPv4(Util, VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.log("Unexpected or invalid packet:") - packet.show() + error("Unexpected or invalid packet:") + error(packet.show()) raise for i in self.interfaces: - remaining_packet = self.get_next_packet_info_for_interface2(i, o, last_info[i]) - self.assertTrue(remaining_packet is None, "Port %u: Packet expected from source %u didn't arrive" % (o, i)) + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Interface %s: Packet expected from interface %s didn't arrive" % + (dst_if.name, i.name)) def test_fib(self): """ IPv4 FIB test """ - for i in self.interfaces: - pkts = self.create_stream(i) - self.pg_add_stream(i, pkts) + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + for i in self.sub_interfaces: + pkts = self.create_stream(i, self.sub_if_packet_sizes) + i.parent.add_stream(pkts) - self.pg_enable_capture(self.interfaces) + self.pg_enable_capture(self.pg_interfaces) self.pg_start() - for i in self.interfaces: - out = self.pg_get_capture(i) - self.log("Verifying capture %u" % i) - self.verify_capture(i, out) + pkts = self.pg0.get_capture() + self.verify_capture(self.pg0, pkts) + + for i in self.sub_interfaces: + pkts = i.parent.get_capture() + self.verify_capture(i, pkts) if __name__ == '__main__': - unittest.main(testRunner = VppTestRunner) + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip6.py b/test/test_ip6.py index 38808a9e..92bb350d 100644 --- a/test/test_ip6.py +++ b/test/test_ip6.py @@ -1,250 +1,126 @@ #!/usr/bin/env python -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) - import unittest +import socket +from logging import * + from framework import VppTestCase, VppTestRunner -from util import Util +from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint from scapy.packet import Raw from scapy.layers.l2 import Ether, Dot1Q -from scapy.layers.inet6 import (IPv6, UDP, - ICMPv6ND_NS, ICMPv6NDOptSrcLLAddr, - ICMPv6ND_NA, ICMPv6NDOptDstLLAddr) +from scapy.layers.inet6 import ICMPv6ND_NS, IPv6, UDP -@unittest.skip('Not finished yet.\n') -class TestIPv6(Util, VppTestCase): +class TestIPv6(VppTestCase): """ IPv6 Test Case """ @classmethod def setUpClass(cls): super(TestIPv6, cls).setUpClass() - try: - cls.create_interfaces_and_subinterfaces() - - # configure IPv6 on hardware interfaces - cls.config_ip6(cls.interfaces) - - cls.config_ip6_on_software_interfaces(cls.interfaces) - - # resolve ICMPv6 ND using hardware interfaces - cls.resolve_icmpv6_nd(cls.interfaces) - - # let VPP know MAC addresses of peer (sub)interfaces - # cls.resolve_icmpv6_nd_on_software_interfaces(cls.interfaces) - cls.send_neighbour_advertisement_on_software_interfaces(cls.interfaces) - - # config 2M FIB enries - #cls.config_fib_entries(2000000) - cls.config_fib_entries(1000000) - - except Exception as e: - super(TestIPv6, cls).tearDownClass() - raise - - def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show hardware") - self.cli(2, "show ip arp") - # self.cli(2, "show ip fib") # 2M entries - self.cli(2, "show error") - self.cli(2, "show run") - - @classmethod - def create_vlan_subif(cls, pg_index, vlan): - cls.api("create_vlan_subif pg%u vlan %u" % (pg_index, vlan)) - - @classmethod - def create_dot1ad_subif(cls, pg_index, sub_id, outer_vlan_id, inner_vlan_id): - cls.api("create_subif pg%u sub_id %u outer_vlan_id %u inner_vlan_id %u dot1ad" - % (pg_index, sub_id, outer_vlan_id, inner_vlan_id)) - - class SoftInt(object): - pass - - class HardInt(SoftInt): - pass - - class Subint(SoftInt): - def __init__(self, sub_id): - self.sub_id = sub_id - - class Dot1QSubint(Subint): - def __init__(self, sub_id, vlan=None): - if vlan is None: - vlan = sub_id - super(TestIPv6.Dot1QSubint, self).__init__(sub_id) - self.vlan = vlan - - class Dot1ADSubint(Subint): - def __init__(self, sub_id, outer_vlan, inner_vlan): - super(TestIPv6.Dot1ADSubint, self).__init__(sub_id) - self.outer_vlan = outer_vlan - self.inner_vlan = inner_vlan - - @classmethod - def create_interfaces_and_subinterfaces(cls): - cls.interfaces = range(3) - - cls.create_interfaces(cls.interfaces) + def setUp(self): + super(TestIPv6, self).setUp() - # Make vpp_api_test see interfaces created using debug CLI (in function create_interfaces) - cls.api("sw_interface_dump") + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) - cls.INT_DETAILS = dict() + # create 2 subinterfaces for p1 and pg2 + self.sub_interfaces = [ + VppDot1QSubint(self, self.pg1, 100), + VppDot1QSubint(self, self.pg2, 200)] + # TODO: VppDot1ADSubint(self, self.pg2, 200, 300, 400) - cls.INT_DETAILS[0] = cls.HardInt() + # packet flows mapping pg0 -> pg1.sub, pg2.sub, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1.sub_if, self.pg2.sub_if] + self.flows[self.pg1.sub_if] = [self.pg0, self.pg2.sub_if] + self.flows[self.pg2.sub_if] = [self.pg0, self.pg1.sub_if] - cls.INT_DETAILS[1] = cls.Dot1QSubint(100) - cls.create_vlan_subif(1, cls.INT_DETAILS[1].vlan) + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] + self.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] - # FIXME: Wrong packet format/wrong layer on output of interface 2 - #self.INT_DETAILS[2] = self.Dot1ADSubint(10, 200, 300) - #self.create_dot1ad_subif(2, self.INT_DETAILS[2].sub_id, self.INT_DETAILS[2].outer_vlan, self.INT_DETAILS[2].inner_vlan) + self.interfaces = list(self.pg_interfaces) + self.interfaces.extend(self.sub_interfaces) - # Use dor1q for now - cls.INT_DETAILS[2] = cls.Dot1QSubint(200) - cls.create_vlan_subif(2, cls.INT_DETAILS[2].vlan) - - for i in cls.interfaces: - det = cls.INT_DETAILS[i] - if isinstance(det, cls.Subint): - cls.api("sw_interface_set_flags pg%u.%u admin-up" % (i, det.sub_id)) - - # IP adresses on subinterfaces - MY_SOFT_IP6S = {} - VPP_SOFT_IP6S = {} - - @classmethod - def config_ip6_on_software_interfaces(cls, args): - for i in args: - cls.MY_SOFT_IP6S[i] = "fd01:%u::2" % i - cls.VPP_SOFT_IP6S[i] = "fd01:%u::1" % i - if isinstance(cls.INT_DETAILS[i], cls.Subint): - interface = "pg%u.%u" % (i, cls.INT_DETAILS[i].sub_id) - else: - interface = "pg%u" % i - cls.api("sw_interface_add_del_address %s %s/32" % (interface, cls.VPP_SOFT_IP6S[i])) - cls.log("My subinterface IPv6 address is %s" % (cls.MY_SOFT_IP6S[i])) - - # let VPP know MAC addresses of peer (sub)interfaces - @classmethod - def resolve_icmpv6_nd_on_software_interfaces(cls, args): - for i in args: - ip = cls.VPP_SOFT_IP6S[i] - cls.log("Sending ICMPv6ND_NS request for %s on port %u" % (ip, i)) - nd_req = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / - IPv6(src=cls.MY_SOFT_IP6S[i], dst=ip) / - ICMPv6ND_NS(tgt=ip) / - ICMPv6NDOptSrcLLAddr(lladdr=cls.MY_MACS[i])) - cls.pg_add_stream(i, nd_req) - cls.pg_enable_capture([i]) - - cls.cli(2, "trace add pg-input 1") - cls.pg_start() - - # We don't need to read output - - # let VPP know MAC addresses of peer (sub)interfaces - @classmethod - def send_neighbour_advertisement_on_software_interfaces(cls, args): - for i in args: - ip = cls.VPP_SOFT_IP6S[i] - cls.log("Sending ICMPv6ND_NA message for %s on port %u" % (ip, i)) - pkt = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / - IPv6(src=cls.MY_SOFT_IP6S[i], dst=ip) / - ICMPv6ND_NA(tgt=ip, R=0, S=0) / - ICMPv6NDOptDstLLAddr(lladdr=cls.MY_MACS[i])) - cls.pg_add_stream(i, pkt) - cls.pg_enable_capture([i]) - - cls.cli(2, "trace add pg-input 1") - cls.pg_start() - - @classmethod - def config_fib_entries(cls, count): - n_int = len(cls.interfaces) - for i in cls.interfaces: - cls.api("ip_add_del_route fd02::1/128 via %s count %u" % (cls.VPP_SOFT_IP6S[i], count / n_int)) - - @classmethod - def add_dot1_layers(cls, i, packet): - assert(type(packet) is Ether) - payload = packet.payload - det = cls.INT_DETAILS[i] - if isinstance(det, cls.Dot1QSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=det.sub_id) / payload) - elif isinstance(det, cls.Dot1ADSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=det.outer_vlan) / Dot1Q(vlan=det.inner_vlan) / payload) - packet.type = 0x88A8 + # setup all interfaces + for i in self.interfaces: + i.admin_up() + i.config_ip6() + i.resolve_ndp() - def remove_dot1_layers(self, i, packet): - self.assertEqual(type(packet), Ether) - payload = packet.payload - det = self.INT_DETAILS[i] - if isinstance(det, self.Dot1QSubint): - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].vlan) - payload = payload.payload - elif isinstance(det, self.Dot1ADSubint): # TODO: change 88A8 type - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].outer_vlan) - payload = payload.payload - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].inner_vlan) - payload = payload.payload - packet.remove_payload() - packet.add_payload(payload) + # config 2M FIB enries + self.config_fib_entries(200) - def create_stream(self, pg_id): - pg_targets = [None] * 3 - pg_targets[0] = [1, 2] - pg_targets[1] = [0, 2] - pg_targets[2] = [0, 1] + def tearDown(self): + super(TestIPv6, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show ip6 neighbors")) + # info(self.vapi.cli("show ip6 fib")) # many entries + + def config_fib_entries(self, count): + n_int = len(self.interfaces) + percent = 0 + counter = 0.0 + dest_addr = socket.inet_pton(socket.AF_INET6, "fd02::1") + dest_addr_len = 128 + for i in self.interfaces: + next_hop_address = i.local_ip6n + for j in range(count / n_int): + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, next_hop_address, is_ipv6=1) + counter = counter + 1 + if counter / count * 100 > percent: + info("Configure %d FIB entries .. %d%% done" % + (count, percent)) + percent = percent + 1 + + def create_stream(self, src_if, packet_sizes): pkts = [] for i in range(0, 257): - target_pg_id = pg_targets[pg_id][i % 2] - info = self.create_packet_info(pg_id, target_pg_id) + dst_if = self.flows[src_if][i % 2] + info = self.create_packet_info( + src_if.sw_if_index, dst_if.sw_if_index) payload = self.info_to_payload(info) - p = (Ether(dst=self.VPP_MACS[pg_id], src=self.MY_MACS[pg_id]) / - IPv6(src=self.MY_SOFT_IP6S[pg_id], dst=self.MY_SOFT_IP6S[target_pg_id]) / + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6) / UDP(sport=1234, dport=1234) / Raw(payload)) info.data = p.copy() - self.add_dot1_layers(pg_id, p) - if not isinstance(self.INT_DETAILS[pg_id], self.Subint): - packet_sizes = [76, 512, 1518, 9018] - else: - packet_sizes = [76, 512, 1518+4, 9018+4] - size = packet_sizes[(i / 2) % len(packet_sizes)] + if isinstance(src_if, VppSubInterface): + p = src_if.add_dot1_layer(p) + size = packet_sizes[(i // 2) % len(packet_sizes)] self.extend_packet(p, size) pkts.append(p) return pkts - def verify_capture(self, o, capture): - last_info = {} + def verify_capture(self, dst_if, capture): + info("Verifying capture on interface %s" % dst_if.name) + last_info = dict() for i in self.interfaces: - last_info[i] = None + last_info[i.sw_if_index] = None + is_sub_if = False + dst_sw_if_index = dst_if.sw_if_index + if hasattr(dst_if, 'parent'): + is_sub_if = True for packet in capture: - self.remove_dot1_layers(o, packet) # Check VLAN tags and Ethernet header + if is_sub_if: + # Check VLAN tags and Ethernet header + packet = dst_if.remove_dot1_layer(packet) self.assertTrue(Dot1Q not in packet) try: ip = packet[IPv6] udp = packet[UDP] payload_info = self.payload_to_info(str(packet[Raw])) packet_index = payload_info.index - src_pg = payload_info.src - dst_pg = payload_info.dst - self.assertEqual(dst_pg, o) - self.log("Got packet on port %u: src=%u (id=%u)" % (o, src_pg, packet_index), 2) - next_info = self.get_next_packet_info_for_interface2(src_pg, dst_pg, last_info[src_pg]) - last_info[src_pg] = next_info + self.assertEqual(payload_info.dst, dst_sw_if_index) + debug("Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info self.assertTrue(next_info is not None) self.assertEqual(packet_index, next_info.index) saved_packet = next_info.data @@ -254,28 +130,37 @@ class TestIPv6(Util, VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.log("Unexpected or invalid packet:") - packet.show() + error("Unexpected or invalid packet:") + error(packet.show()) raise for i in self.interfaces: - remaining_packet = self.get_next_packet_info_for_interface2(i, o, last_info[i]) - self.assertTrue(remaining_packet is None, "Port %u: Packet expected from source %u didn't arrive" % (o, i)) + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Interface %s: Packet expected from interface %s didn't arrive" % + (dst_if.name, i.name)) def test_fib(self): """ IPv6 FIB test """ - for i in self.interfaces: - pkts = self.create_stream(i) - self.pg_add_stream(i, pkts) + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + for i in self.sub_interfaces: + pkts = self.create_stream(i, self.sub_if_packet_sizes) + i.parent.add_stream(pkts) - self.pg_enable_capture(self.interfaces) + self.pg_enable_capture(self.pg_interfaces) self.pg_start() - for i in self.interfaces: - out = self.pg_get_capture(i) - self.log("Verifying capture %u" % i) - self.verify_capture(i, out) + pkts = self.pg0.get_capture() + self.verify_capture(self.pg0, pkts) + + for i in self.sub_interfaces: + pkts = i.parent.get_capture() + self.verify_capture(i, pkts) if __name__ == '__main__': - unittest.main(testRunner = VppTestRunner) + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2bd.py b/test/test_l2bd.py index c2b73a4d..0fced5d9 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -1,438 +1,187 @@ #!/usr/bin/env python -## @file test_l2bd.py -# Module to provide L2 bridge domain test case. -# -# The module provides a set of tools for L2 bridge domain tests. - -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) import unittest +from logging import * import random -from framework import * -from scapy.all import * +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppDot1QSubint +from util import TestHost -## Subclass of the VppTestCase class. -# -# This subclass is a class for L2 bridge domain test cases. It provides methods -# to create interfaces, configure L2 bridge domain, create and verify packet -# streams. class TestL2bd(VppTestCase): """ L2BD Test Case """ - ## Test variables - interf_nr = 3 # Number of interfaces - bd_id = 1 # Bridge domain ID - mac_entries = 100 # Number of MAC entries for bridge-domain to learn - dot1q_sub_id = 100 # SubID of dot1q sub-interface - dot1q_tag = 100 # VLAN tag for dot1q sub-interface - dot1ad_sub_id = 200 # SubID of dot1ad sub-interface - dot1ad_outer_tag = 200 # VLAN S-tag for dot1ad sub-interface - dot1ad_inner_tag = 300 # VLAN C-tag for dot1ad sub-interface - pkts_per_burst = 257 # Number of packets per burst + # Test variables + bd_id = 1 # Bridge domain ID + mac_entries_count = 100 # Number of MAC entries for bridge-domain to learn + dot1q_sub_id = 100 # SubID of dot1q sub-interface + dot1q_tag = 100 # VLAN tag for dot1q sub-interface + dot1ad_sub_id = 200 # SubID of dot1ad sub-interface + dot1ad_outer_tag = 200 # VLAN S-tag for dot1ad sub-interface + dot1ad_inner_tag = 300 # VLAN C-tag for dot1ad sub-interface + pkts_per_burst = 257 # Number of packets per burst - ## Class method to start the test case. - # Overrides setUpClass method in VppTestCase class. - # Python try..except statement is used to ensure that the tear down of - # the class will be executed even if exception is raised. - # @param cls The class pointer. @classmethod def setUpClass(cls): super(TestL2bd, cls).setUpClass() - try: - ## Create interfaces and sub-interfaces - cls.create_interfaces_and_subinterfaces(TestL2bd.interf_nr) - - ## Create BD with MAC learning enabled and put interfaces and - # sub-interfaces to this BD - cls.api("bridge_domain_add_del bd_id %u learn 1" % TestL2bd.bd_id) - for i in cls.interfaces: - if isinstance(cls.INT_DETAILS[i], cls.Subint): - interface = "pg%u.%u" % (i, cls.INT_DETAILS[i].sub_id) - else: - interface = "pg%u" % i - cls.api("sw_interface_set_l2_bridge %s bd_id %u" - % (interface, TestL2bd.bd_id)) - - ## Make the BD learn a number of MAC entries specified by the test - # variable . - cls.create_mac_entries(TestL2bd.mac_entries) - cls.cli(0, "show l2fib") - - except Exception as e: - super(TestL2bd, cls).tearDownClass() - raise e - - ## Method to define tear down VPP actions of the test case. - # Overrides tearDown method in VppTestCase class. - # @param self The object pointer. - def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show hardware") - self.cli(2, "show l2fib verbose") - self.cli(2, "show error") - self.cli(2, "show run") - self.cli(2, "show bridge-domain 1 detail") - - ## Class method to create VLAN sub-interface. - # Uses VPP API command to create VLAN sub-interface. - # @param cls The class pointer. - # @param pg_index Integer variable to store the index of the packet - # generator interface to create VLAN sub-interface on. - # @param vlan_id Integer variable to store required VLAN tag value. - @classmethod - def create_vlan_subif(cls, pg_index, vlan_id): - cls.api("create_vlan_subif pg%u vlan %u" % (pg_index, vlan_id)) - - ## Class method to create dot1ad sub-interface. - # Use VPP API command to create dot1ad sub-interface. - # @param cls The class pointer. - # @param pg_index Integer variable to store the index of the packet - # generator interface to create dot1ad sub-interface on. - # @param outer_vlan_id Integer variable to store required outer VLAN tag - # value (S-TAG). - # @param inner_vlan_id Integer variable to store required inner VLAN tag - # value (C-TAG). - @classmethod - def create_dot1ad_subif(cls, pg_index, sub_id, outer_vlan_id, - inner_vlan_id): - cls.api("create_subif pg%u sub_id %u outer_vlan_id %u inner_vlan_id" - " %u dot1ad" % (pg_index, sub_id, outer_vlan_id, inner_vlan_id)) - - ## Base class for interface. - # To define object representation of the interface. - class Interface(object): - pass + def setUp(self): + super(TestL2bd, self).setUp() - ## Sub-class of the interface class. - # To define object representation of the HW interface. - class HardInt(Interface): - pass + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) - ## Sub-class of the interface class. - # To define object representation of the SW interface. - class SoftInt(Interface): - pass + # create 2 sub-interfaces for pg1 and pg2 + self.sub_interfaces = [ + VppDot1QSubint(self, self.pg1, TestL2bd.dot1q_sub_id), + VppDot1QSubint(self, self.pg2, TestL2bd.dot1ad_sub_id)] - ## Sub-class of the SW interface class. - # To represent the general sub-interface. - class Subint(SoftInt): - ## The constructor. - # @param sub_id Integer variable to store sub-interface ID. - def __init__(self, sub_id): - self.sub_id = sub_id - - ## Sub-class of the SW interface class. - # To represent dot1q sub-interface. - class Dot1QSubint(Subint): - ## The constructor. - # @param sub_id Integer variable to store sub-interface ID. - # @param vlan Integer variable (optional) to store VLAN tag value. Set - # to sub_id value when VLAN tag value not provided. - def __init__(self, sub_id, vlan=None): - if vlan is None: - vlan = sub_id - super(TestL2bd.Dot1QSubint, self).__init__(sub_id) - self.vlan = vlan - - ## Sub-class of the SW interface class. - # To represent dot1ad sub-interface. - class Dot1ADSubint(Subint): - ## The constructor. - # @param sub_id Integer variable to store sub-interface ID. - # @param outer_vlan Integer variable to store outer VLAN tag value. - # @param inner_vlan Integer variable to store inner VLAN tag value. - def __init__(self, sub_id, outer_vlan, inner_vlan): - super(TestL2bd.Dot1ADSubint, self).__init__(sub_id) - self.outer_vlan = outer_vlan - self.inner_vlan = inner_vlan - - ## Class method to create interfaces and sub-interfaces. - # Current implementation: create three interfaces, then create Dot1Q - # sub-interfaces for the second and the third interface with VLAN tags - # equal to their sub-interface IDs. Set sub-interfaces status to admin-up. - # @param cls The class pointer. - # @param int_nr Integer variable to store the number of interfaces to be - # created. - # TODO: Parametrize required numbers of dot1q and dot1ad to be created. - @classmethod - def create_interfaces_and_subinterfaces(cls, int_nr): - ## A class list variable to store interface indexes. - cls.interfaces = range(int_nr) + # packet flows mapping pg0 -> pg1, pg2, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1, self.pg2] + self.flows[self.pg1] = [self.pg0, self.pg2] + self.flows[self.pg2] = [self.pg0, self.pg1] - # Create interfaces - cls.create_interfaces(cls.interfaces) + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] + self.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] - # Make vpp_api_test see interfaces created using debug CLI (in function - # create_interfaces) - cls.api("sw_interface_dump") + self.interfaces = list(self.pg_interfaces) + self.interfaces.extend(self.sub_interfaces) - ## A class dictionary variable to store data about interfaces. - # First create an empty dictionary then store interface data there. - cls.INT_DETAILS = dict() + # Create BD with MAC learning enabled and put interfaces and + # sub-interfaces to this BD + for pg_if in self.pg_interfaces: + sw_if_index = pg_if.sub_if.sw_if_index if hasattr(pg_if, 'sub_if') \ + else pg_if.sw_if_index + self.vapi.sw_interface_set_l2_bridge(sw_if_index, + bd_id=TestL2bd.bd_id) - # 1st interface is untagged - no sub-interface required - cls.INT_DETAILS[0] = cls.HardInt() + # setup all interfaces + for i in self.interfaces: + i.admin_up() - # 2nd interface is dot1q tagged - cls.INT_DETAILS[1] = cls.Dot1QSubint(TestL2bd.dot1q_sub_id, - TestL2bd.dot1q_tag) - cls.create_vlan_subif(1, cls.INT_DETAILS[1].vlan) + # mapping between packet-generator index and lists of test hosts + self.hosts_by_pg_idx = dict() - # 3rd interface is dot1ad tagged - # FIXME: Wrong packet format/wrong layer on output of interface 2 - #self.INT_DETAILS[2] = self.Dot1ADSubint(TestL2bd.dot1ad_sub_id, TestL2bd.dot1ad_outer_tag, TestL2bd.dot1ad_inner_tag) - #self.create_dot1ad_subif(2, self.INT_DETAILS[2].sub_id, self.INT_DETAILS[2].outer_vlan, self.INT_DETAILS[2].inner_vlan) + # create test host entries and inject packets to learn MAC entries in + # the bridge-domain + self.create_hosts_and_learn(TestL2bd.mac_entries_count) + info(self.vapi.cli("show l2fib")) - # Use dot1q for now. - cls.INT_DETAILS[2] = cls.Dot1QSubint(TestL2bd.dot1ad_sub_id, - TestL2bd.dot1ad_outer_tag) - cls.create_vlan_subif(2, cls.INT_DETAILS[2].vlan) + def tearDown(self): + super(TestL2bd, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show l2fib verbose")) + info(self.vapi.cli("show bridge-domain %s detail" % self.bd_id)) - for i in cls.interfaces: - if isinstance(cls.INT_DETAILS[i], cls.Subint): - cls.api("sw_interface_set_flags pg%u.%u admin-up" - % (i, cls.INT_DETAILS[i].sub_id)) - ## @var interfaces - # List variable to store interface indexes. - ## @var INT_DETAILS - # Dictionary variable to store data about interfaces. + def create_hosts_and_learn(self, count): + """ + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address. Create + L2 MAC packet stream with host MAC addresses per interface to let + the bridge domain learn these MAC addresses. - ## Class method for bridge-domain to learn defined number of MAC addresses. - # Create required number of host MAC addresses and distribute them among - # interfaces. Create host IPv4 address for every host MAC address. Create - # L2 MAC packet stream with host MAC addresses per interface to let - # the bridge domain learn these MAC addresses. - # @param cls The class pointer. - # @param count Integer variable to store the number of MAC addresses to be - # created. - @classmethod - def create_mac_entries(cls, count): - n_int = len(cls.interfaces) + :param count: Integer number of hosts to create MAC/IPv4 addresses for. + """ + n_int = len(self.pg_interfaces) macs_per_if = count / n_int - for i in cls.interfaces: - start_nr = macs_per_if*i - end_nr = count if i == (n_int - 1) else macs_per_if*(i+1) - cls.MY_MACS[i] = [] - cls.MY_IP4S[i] = [] + i = -1 + for pg_if in self.pg_interfaces: + i += 1 + start_nr = macs_per_if * i + end_nr = count if i == (n_int - 1) else macs_per_if * (i + 1) + self.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] packets = [] for j in range(start_nr, end_nr): - cls.MY_MACS[i].append("00:00:00:ff:%02x:%02x" % (i, j)) - cls.MY_IP4S[i].append("172.17.1%02x.%u" % (i, j)) - packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i])) + host = TestHost( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac)) + hosts.append(host) + if hasattr(pg_if, 'sub_if'): + packet = pg_if.sub_if.add_dot1_layer(packet) packets.append(packet) - cls.pg_add_stream(i, packets) - # Based on the verbosity level set in the system print the log. - cls.log("Sending broadcast eth frames for MAC learning", 1) - cls.pg_start() - # Packet stream capturing is not started as we don't need to read - # the output. - ## @var n_int - # Integer variable to store the number of interfaces. - ## @var macs_per_if - # Integer variable to store the number of MAC addresses per interface. - ## @var start_nr - # Integer variable to store the starting number of the range used to - # generate MAC addresses for the interface. - ## @var end_nr - # Integer variable to store the ending number of the range used to - # generate MAC addresses for the interface. - ## @var MY_MACS - # Dictionary variable to store list of MAC addresses per interface. - ## @var MY_IP4S - # Dictionary variable to store list of IPv4 addresses per interface. - - ## Class method to add dot1q or dot1ad layer to the packet. - # Based on sub-interface data of the defined interface add dot1q or dot1ad - # Ethernet header layer to the packet. - # @param cls The class pointer. - # @param i Integer variable to store the index of the interface. - # @param packet Object variable to store the packet where to add dot1q or - # dot1ad layer. - # TODO: Move this class method to utils.py. - @classmethod - def add_dot1_layers(cls, i, packet): - assert(type(packet) is Ether) - payload = packet.payload - if isinstance(cls.INT_DETAILS[i], cls.Dot1QSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=cls.INT_DETAILS[i].vlan) / payload) - elif isinstance(cls.INT_DETAILS[i], cls.Dot1ADSubint): - packet.remove_payload() - packet.add_payload(Dot1Q(vlan=cls.INT_DETAILS[i].outer_vlan, - type=0x8100) / - Dot1Q(vlan=cls.INT_DETAILS[i].inner_vlan) / - payload) - packet.type = 0x88A8 - ## @var payload - # Object variable to store payload of the packet. - ## @var INT_DETAILS - # Dictionary variable to store data about interfaces. - ## @var Dot1QSubint - # Class variable representing dot1q sub-interfaces. - ## @var Dot1ADSubint - # Class variable representing dot1ad sub-interfaces. - - ## Method to remove dot1q or dot1ad layer from the packet. - # Based on sub-interface data of the defined interface remove dot1q or - # dot1ad layer from the packet. - # @param cls The class pointer. - # @param i Integer variable to store the index of the interface. - # @param packet Object variable to store the packet where to remove dot1q - # or dot1ad layer. - def remove_dot1_layers(self, i, packet): - self.assertEqual(type(packet), Ether) - payload = packet.payload - if isinstance(self.INT_DETAILS[i], self.Dot1QSubint): - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].vlan) - payload = payload.payload - elif isinstance(self.INT_DETAILS[i], self.Dot1ADSubint): # TODO: change 88A8 type - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].outer_vlan) - payload = payload.payload - self.assertEqual(type(payload), Dot1Q) - self.assertEqual(payload.vlan, self.INT_DETAILS[i].inner_vlan) - payload = payload.payload - packet.remove_payload() - packet.add_payload(payload) - ## @var payload - # Object variable to store payload of the packet. - ## @var INT_DETAILS - # Dictionary variable to store data about interfaces. - ## @var Dot1QSubint - # Class variable representing dot1q sub-interfaces. - ## @var Dot1ADSubint - # Class variable representing dot1ad sub-interfaces. + pg_if.add_stream(packets) + info("Sending broadcast eth frames for MAC learning") + self.pg_start() - ## Method to create packet stream for the packet generator interface. - # Create input packet stream for the given packet generator interface with - # packets of different length targeted for all other created packet - # generator interfaces. - # @param self The object pointer. - # @param pg_id Integer variable to store the index of the interface to - # create the input packet stream. - # @return pkts List variable to store created input stream of packets. - def create_stream(self, pg_id): - # TODO: use variables to create lists based on interface number - pg_targets = [None] * 3 - pg_targets[0] = [1, 2] - pg_targets[1] = [0, 2] - pg_targets[2] = [0, 1] + def create_stream(self, src_if, packet_sizes): pkts = [] for i in range(0, TestL2bd.pkts_per_burst): - target_pg_id = pg_targets[pg_id][i % 2] - target_host_id = random.randrange(len(self.MY_MACS[target_pg_id])) - source_host_id = random.randrange(len(self.MY_MACS[pg_id])) - pkt_info = self.create_packet_info(pg_id, target_pg_id) + dst_if = self.flows[src_if][i % 2] + dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) + src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) + pkt_info = self.create_packet_info( + src_if.sw_if_index, dst_if.sw_if_index) payload = self.info_to_payload(pkt_info) - p = (Ether(dst=self.MY_MACS[target_pg_id][target_host_id], - src=self.MY_MACS[pg_id][source_host_id]) / - IP(src=self.MY_IP4S[pg_id][source_host_id], - dst=self.MY_IP4S[target_pg_id][target_host_id]) / + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / UDP(sport=1234, dport=1234) / Raw(payload)) pkt_info.data = p.copy() - self.add_dot1_layers(pg_id, p) - if not isinstance(self.INT_DETAILS[pg_id], self.Subint): - packet_sizes = [64, 512, 1518, 9018] - else: - packet_sizes = [64, 512, 1518+4, 9018+4] + if hasattr(src_if, 'sub_if'): + p = src_if.sub_if.add_dot1_layer(p) size = packet_sizes[(i / 2) % len(packet_sizes)] self.extend_packet(p, size) pkts.append(p) return pkts - ## @var pg_targets - # List variable to store list of indexes of target packet generator - # interfaces for every source packet generator interface. - ## @var target_pg_id - # Integer variable to store the index of the random target packet - # generator interfaces. - ## @var target_host_id - # Integer variable to store the index of the randomly chosen - # destination host MAC/IPv4 address. - ## @var source_host_id - # Integer variable to store the index of the randomly chosen source - # host MAC/IPv4 address. - ## @var pkt_info - # Object variable to store the information about the generated packet. - ## @var payload - # String variable to store the payload of the packet to be generated. - ## @var p - # Object variable to store the generated packet. - ## @var packet_sizes - # List variable to store required packet sizes. - ## @var size - # List variable to store required packet sizes. - ## Method to verify packet stream received on the packet generator interface. - # Verify packet-by-packet the output stream captured on a given packet - # generator (pg) interface using following packet payload data - order of - # packet in the stream, index of the source and destination pg interface, - # src and dst host IPv4 addresses and src port and dst port values of UDP - # layer. - # @param self The object pointer. - # @param o Integer variable to store the index of the interface to - # verify the output packet stream. - # @param capture List variable to store the captured output packet stream. - def verify_capture(self, o, capture): - last_info = {} - for i in self.interfaces: - last_info[i] = None + def verify_capture(self, pg_if, capture): + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index for packet in capture: + payload_info = self.payload_to_info(str(packet[Raw])) + src_sw_if_index = payload_info.src + src_if = None + for ifc in self.pg_interfaces: + if ifc != pg_if: + if ifc.sw_if_index == src_sw_if_index: + src_if = ifc + break + if hasattr(src_if, 'sub_if'): + # Check VLAN tags and Ethernet header + packet = src_if.sub_if.remove_dot1_layer(packet) + self.assertTrue(Dot1Q not in packet) try: ip = packet[IP] udp = packet[UDP] - payload_info = self.payload_to_info(str(packet[Raw])) - # Check VLAN tags and Ethernet header - # TODO: Rework to check VLAN tag(s) and do not remove them - self.remove_dot1_layers(payload_info.src, packet) - self.assertTrue(Dot1Q not in packet) - self.assertEqual(payload_info.dst, o) - self.log("Got packet on port %u: src=%u (id=%u)" - % (o, payload_info.src, payload_info.index), 2) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) next_info = self.get_next_packet_info_for_interface2( - payload_info.src, payload_info.dst, + payload_info.src, dst_sw_if_index, last_info[payload_info.src]) last_info[payload_info.src] = next_info self.assertTrue(next_info is not None) - self.assertEqual(payload_info.index, next_info.index) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data # Check standard fields - self.assertEqual(ip.src, next_info.data[IP].src) - self.assertEqual(ip.dst, next_info.data[IP].dst) - self.assertEqual(udp.sport, next_info.data[UDP].sport) - self.assertEqual(udp.dport, next_info.data[UDP].dport) + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.log("Unexpected or invalid packet:") - packet.show() + error("Unexpected or invalid packet:") + error(packet.show()) raise - for i in self.interfaces: + for i in self.pg_interfaces: remaining_packet = self.get_next_packet_info_for_interface2( - i, o, last_info[i]) - self.assertTrue(remaining_packet is None, - "Port %u: Packet expected from source %u didn't" - " arrive" % (o, i)) - ## @var last_info - # Dictionary variable to store verified packets per packet generator - # interface. - ## @var ip - # Object variable to store the IP layer of the packet. - ## @var udp - # Object variable to store the UDP layer of the packet. - ## @var payload_info - # Object variable to store required information about the packet. - ## @var next_info - # Object variable to store information about next packet. - ## @var remaining_packet - # Object variable to store information about remaining packet. + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) - ## Method defining VPP L2 bridge domain test case. - # Contains execution steps of the test case. - # @param self The object pointer. def test_l2bd(self): """ L2BD MAC learning test @@ -447,27 +196,23 @@ class TestL2bd(VppTestCase): burst of 257 pkts per interface """ - ## Create incoming packet streams for packet-generator interfaces - for i in self.interfaces: - pkts = self.create_stream(i) - self.pg_add_stream(i, pkts) + # Create incoming packet streams for packet-generator interfaces + for i in self.pg_interfaces: + packet_sizes = self.sub_if_packet_sizes if hasattr(i, 'sub_if') \ + else self.pg_if_packet_sizes + pkts = self.create_stream(i, packet_sizes) + i.add_stream(pkts) - ## Enable packet capture and start packet sending - self.pg_enable_capture(self.interfaces) + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) self.pg_start() - ## Verify outgoing packet streams per packet-generator interface - for i in self.interfaces: - out = self.pg_get_capture(i) - self.log("Verifying capture %u" % i) - self.verify_capture(i, out) - ## @var pkts - # List variable to store created input stream of packets for the packet - # generator interface. - ## @var out - # List variable to store captured output stream of packets for - # the packet generator interface. + # Verify outgoing packet streams per packet-generator interface + for i in self.pg_interfaces: + capture = i.get_capture() + info("Verifying capture on interface %s" % i.name) + self.verify_capture(i, capture) if __name__ == '__main__': - unittest.main(testRunner = VppTestRunner) + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2xc.py b/test/test_l2xc.py index f5fc8743..d448a04d 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -1,210 +1,152 @@ #!/usr/bin/env python -## @file test_l2xc.py -# Module to provide L2 cross-connect test case. -# -# The module provides a set of tools for L2 cross-connect tests. - -import logging -logging.getLogger("scapy.runtime").setLevel(logging.ERROR) import unittest import random -from framework import VppTestCase, VppTestRunner -from scapy.layers.l2 import Ether, Raw + +from scapy.packet import Raw +from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP +from logging import * + +from framework import VppTestCase, VppTestRunner +from util import TestHost -## Subclass of the VppTestCase class. -# -# This subclass is a class for L2 cross-connect test cases. It provides methods -# to create interfaces, configuring L2 cross-connects, creating and verifying -# packet streams. class TestL2xc(VppTestCase): """ L2XC Test Case """ # Test variables - interf_nr = 4 # Number of interfaces hosts_nr = 10 # Number of hosts pkts_per_burst = 257 # Number of packets per burst - ## Class method to start the test case. - # Overrides setUpClass method in VppTestCase class. - # There is used try..except statement to ensure that the tear down of - # the class will be executed even if any exception is raised. - # @param cls The class pointer. @classmethod def setUpClass(cls): super(TestL2xc, cls).setUpClass() - try: - ## Create interfaces - cls.interfaces = range(TestL2xc.interf_nr) - cls.create_interfaces(cls.interfaces) + def setUp(self): + super(TestL2xc, self).setUp() - ## Create bi-directional cross-connects between pg0 and pg1 - cls.api("sw_interface_set_l2_xconnect rx pg0 tx pg1 enable") - cls.api("sw_interface_set_l2_xconnect rx pg1 tx pg0 enable") + # create 4 pg interfaces + self.create_pg_interfaces(range(4)) - ## Create bi-directional cross-connects between pg2 and pg3 - cls.api("sw_interface_set_l2_xconnect rx pg2 tx pg3 enable") - cls.api("sw_interface_set_l2_xconnect rx pg3 tx pg2 enable") + # packet flows mapping pg0 -> pg1, pg2 -> pg3, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1] + self.flows[self.pg1] = [self.pg0] + self.flows[self.pg2] = [self.pg3] + self.flows[self.pg3] = [self.pg2] - cls.cli(0, "show l2patch") + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] - ## Create host MAC and IPv4 lists - cls.create_host_lists(TestL2xc.hosts_nr) + self.interfaces = list(self.pg_interfaces) - except Exception as e: - cls.tearDownClass() - raise e + # Create bi-directional cross-connects between pg0 and pg1 + self.vapi.sw_interface_set_l2_xconnect( + self.pg0.sw_if_index, self.pg1.sw_if_index, enable=1) + self.vapi.sw_interface_set_l2_xconnect( + self.pg1.sw_if_index, self.pg0.sw_if_index, enable=1) + + # Create bi-directional cross-connects between pg2 and pg3 + self.vapi.sw_interface_set_l2_xconnect( + self.pg2.sw_if_index, self.pg3.sw_if_index, enable=1) + self.vapi.sw_interface_set_l2_xconnect( + self.pg3.sw_if_index, self.pg2.sw_if_index, enable=1) + + info(self.vapi.cli("show l2patch")) + + # mapping between packet-generator index and lists of test hosts + self.hosts_by_pg_idx = dict() + + # Create host MAC and IPv4 lists + # self.MY_MACS = dict() + # self.MY_IP4S = dict() + self.create_host_lists(TestL2xc.hosts_nr) + + # setup all interfaces + for i in self.interfaces: + i.admin_up() - ## Method to define tear down VPP actions of the test case. - # Overrides tearDown method in VppTestCase class. - # @param self The object pointer. def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show hardware") - self.cli(2, "show l2patch") - self.cli(2, "show error") - self.cli(2, "show run") - - ## Class method to create required number of MAC and IPv4 addresses. - # Create required number of host MAC addresses and distribute them among - # interfaces. Create host IPv4 address for every host MAC address too. - # @param cls The class pointer. - # @param count Integer variable to store the number of MAC addresses to be - # created. - @classmethod - def create_host_lists(cls, count): - for i in cls.interfaces: - cls.MY_MACS[i] = [] - cls.MY_IP4S[i] = [] + super(TestL2xc, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show l2patch")) + + def create_host_lists(self, count): + """ Method to create required number of MAC and IPv4 addresses. + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address too. + + :param count: Number of hosts to create MAC and IPv4 addresses for. + Type: int + """ + for pg_if in self.pg_interfaces: + # self.MY_MACS[i.sw_if_index] = [] + # self.MY_IP4S[i.sw_if_index] = [] + self.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] for j in range(0, count): - cls.MY_MACS[i].append("00:00:00:ff:%02x:%02x" % (i, j)) - cls.MY_IP4S[i].append("172.17.1%02x.%u" % (i, j)) - ## @var MY_MACS - # Dictionary variable to store list of MAC addresses per interface. - ## @var MY_IP4S - # Dictionary variable to store list of IPv4 addresses per interface. - - ## Method to create packet stream for the packet generator interface. - # Create input packet stream for the given packet generator interface with - # packets of different length targeted for all other created packet - # generator interfaces. - # @param self The object pointer. - # @param pg_id Integer variable to store the index of the interface to - # create the input packet stream. - # @return pkts List variable to store created input stream of packets. - def create_stream(self, pg_id): - # TODO: use variables to create lists based on interface number - pg_targets = [None] * 4 - pg_targets[0] = [1] - pg_targets[1] = [0] - pg_targets[2] = [3] - pg_targets[3] = [2] + host = TestHost( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + hosts.append(host) + + def create_stream(self, src_if, packet_sizes): pkts = [] for i in range(0, TestL2xc.pkts_per_burst): - target_pg_id = pg_targets[pg_id][0] - target_host_id = random.randrange(len(self.MY_MACS[target_pg_id])) - source_host_id = random.randrange(len(self.MY_MACS[pg_id])) - pkt_info = self.create_packet_info(pg_id, target_pg_id) + dst_if = self.flows[src_if][0] + dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) + src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) + pkt_info = self.create_packet_info( + src_if.sw_if_index, dst_if.sw_if_index) payload = self.info_to_payload(pkt_info) - p = (Ether(dst=self.MY_MACS[target_pg_id][target_host_id], - src=self.MY_MACS[pg_id][source_host_id]) / - IP(src=self.MY_IP4S[pg_id][source_host_id], - dst=self.MY_IP4S[target_pg_id][target_host_id]) / + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / UDP(sport=1234, dport=1234) / Raw(payload)) pkt_info.data = p.copy() - packet_sizes = [64, 512, 1518, 9018] size = packet_sizes[(i / 2) % len(packet_sizes)] self.extend_packet(p, size) pkts.append(p) return pkts - ## @var pg_targets - # List variable to store list of indexes of target packet generator - # interfaces for every source packet generator interface. - ## @var target_pg_id - # Integer variable to store the index of the random target packet - # generator interfaces. - ## @var target_host_id - # Integer variable to store the index of the randomly chosen - # destination host MAC/IPv4 address. - ## @var source_host_id - # Integer variable to store the index of the randomly chosen source - # host MAC/IPv4 address. - ## @var pkt_info - # Object variable to store the information about the generated packet. - ## @var payload - # String variable to store the payload of the packet to be generated. - ## @var p - # Object variable to store the generated packet. - ## @var packet_sizes - # List variable to store required packet sizes. - ## @var size - # List variable to store required packet sizes. - - ## Method to verify packet stream received on the packet generator interface. - # Verify packet-by-packet the output stream captured on a given packet - # generator (pg) interface using following packet payload data - order of - # packet in the stream, index of the source and destination pg interface, - # src and dst host IPv4 addresses and src port and dst port values of UDP - # layer. - # @param self The object pointer. - # @param o Integer variable to store the index of the interface to - # verify the output packet stream. - # @param capture List variable to store the captured output packet stream. - def verify_capture(self, o, capture): - last_info = {} + + def verify_capture(self, pg_if, capture): + last_info = dict() for i in self.interfaces: - last_info[i] = None + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index for packet in capture: try: ip = packet[IP] udp = packet[UDP] payload_info = self.payload_to_info(str(packet[Raw])) - self.assertEqual(payload_info.dst, o) - self.log("Got packet on port %u: src=%u (id=%u)" - % (o, payload_info.src, payload_info.index), 2) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) next_info = self.get_next_packet_info_for_interface2( - payload_info.src, payload_info.dst, + payload_info.src, dst_sw_if_index, last_info[payload_info.src]) last_info[payload_info.src] = next_info self.assertTrue(next_info is not None) - self.assertEqual(payload_info.index, next_info.index) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data # Check standard fields - self.assertEqual(ip.src, next_info.data[IP].src) - self.assertEqual(ip.dst, next_info.data[IP].dst) - self.assertEqual(udp.sport, next_info.data[UDP].sport) - self.assertEqual(udp.dport, next_info.data[UDP].dport) + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.log("Unexpected or invalid packet:") + error("Unexpected or invalid packet:") packet.show() raise for i in self.interfaces: remaining_packet = self.get_next_packet_info_for_interface2( - i, o, last_info[i]) + i, dst_sw_if_index, last_info[i.sw_if_index]) self.assertTrue(remaining_packet is None, "Port %u: Packet expected from source %u didn't" - " arrive" % (o, i)) - ## @var last_info - # Dictionary variable to store verified packets per packet generator - # interface. - ## @var ip - # Object variable to store the IP layer of the packet. - ## @var udp - # Object variable to store the UDP layer of the packet. - ## @var payload_info - # Object variable to store required information about the packet. - ## @var next_info - # Object variable to store information about next packet. - ## @var remaining_packet - # Object variable to store information about remaining packet. - - ## Method defining L2 cross-connect test case. - # Contains steps of the test case. - # @param self The object pointer. + " arrive" % (dst_sw_if_index, i.sw_if_index)) + def test_l2xc(self): """ L2XC test @@ -217,27 +159,21 @@ class TestL2xc(VppTestCase): burst of packets per interface """ - ## Create incoming packet streams for packet-generator interfaces + # Create incoming packet streams for packet-generator interfaces for i in self.interfaces: - pkts = self.create_stream(i) - self.pg_add_stream(i, pkts) + pkts = self.create_stream(i, self.pg_if_packet_sizes) + i.add_stream(pkts) - ## Enable packet capturing and start packet sending - self.pg_enable_capture(self.interfaces) + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) self.pg_start() - ## Verify outgoing packet streams per packet-generator interface - for i in self.interfaces: - out = self.pg_get_capture(i) - self.log("Verifying capture %u" % i) - self.verify_capture(i, out) - ## @var pkts - # List variable to store created input stream of packets for the packet - # generator interface. - ## @var out - # List variable to store captured output stream of packets for - # the packet generator interface. + # Verify outgoing packet streams per packet-generator interface + for i in self.pg_interfaces: + capture = i.get_capture() + info("Verifying capture on interface %s" % i.name) + self.verify_capture(i, capture) if __name__ == '__main__': - unittest.main(testRunner = VppTestRunner) + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_lb.py b/test/test_lb.py index eb308195..76fdd693 100644 --- a/test/test_lb.py +++ b/test/test_lb.py @@ -1,27 +1,30 @@ -import unittest -import time import socket -from framework import VppTestCase, VppTestRunner -from util import Util +import unittest +from logging import * -from scapy.packet import Raw -from scapy.layers.l2 import Ether, GRE from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether, GRE +from scapy.packet import Raw + +from framework import VppTestCase + +""" TestLB is a subclass of VPPTestCase classes. + + TestLB class defines Load Balancer test cases for: + - IP4 to GRE4 encap + - IP4 to GRE6 encap + - IP6 to GRE4 encap + - IP6 to GRE6 encap + + As stated in comments below, GRE has issues with IPv6. + All test cases involving IPv6 are executed, but + received packets are not parsed and checked. + +""" -## TestLB is a subclass of Util and VPPTestCase classes. -# -# TestLB class defines Load Balancer test cases for: -# - IP4 to GRE4 encap -# - IP4 to GRE6 encap -# - IP6 to GRE4 encap -# - IP6 to GRE6 encap -# -# As stated in comments below, GRE has issues with IPv6. -# All test cases involving IPv6 are executed, but -# received packets are not parsed and checked. -# -class TestLB(Util, VppTestCase): + +class TestLB(VppTestCase): """ Load Balancer Test Case """ @classmethod @@ -32,61 +35,73 @@ class TestLB(Util, VppTestCase): cls.packets = range(100) try: - cls.create_interfaces([0,1]) - cls.api("sw_interface_dump") - cls.config_ip4([0,1]) - cls.config_ip6([0,1]) - cls.resolve_arp([0,1]) - cls.resolve_icmpv6_nd([0,1]) - cls.cli(0, "ip route add 10.0.0.0/24 via %s pg1" % (cls.MY_IP4S[1])) - cls.cli(0, "ip route add 2002::/16 via %s pg1" % (cls.MY_IP6S[1])) - cls.cli(0, "lb conf buckets-log2 20 ip4-src-address 39.40.41.42 ip6-src-address fd00:f00d::1") - - except Exception as e: + cls.create_pg_interfaces(range(2)) + cls.interfaces = list(cls.pg_interfaces) + + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.config_ip6() + i.disable_ipv6_ra() + i.resolve_arp() + i.resolve_ndp() + dst4 = socket.inet_pton(socket.AF_INET, "10.0.0.0") + dst6 = socket.inet_pton(socket.AF_INET6, "2002::") + cls.vapi.ip_add_del_route(dst4, 24, cls.pg1.remote_ip4n) + cls.vapi.ip_add_del_route(dst6, 16, cls.pg1.remote_ip6n, is_ipv6=1) + cls.vapi.cli("lb conf ip4-src-address 39.40.41.42") + cls.vapi.cli("lb conf ip6-src-address 2004::1") + except Exception: super(TestLB, cls).tearDownClass() raise def tearDown(self): - self.cli(2, "show int") - self.cli(2, "show trace") - self.cli(2, "show lb vip verbose") + super(TestLB, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show lb vip verbose")) def getIPv4Flow(self, id): return (IP(dst="90.0.%u.%u" % (id / 255, id % 255), - src="40.0.%u.%u" % (id / 255, id % 255)) / + src="40.0.%u.%u" % (id / 255, id % 255)) / UDP(sport=10000 + id, dport=20000 + id)) def getIPv6Flow(self, id): return (IPv6(dst="2001::%u" % (id), src="fd00:f00d:ffff::%u" % (id)) / - UDP(sport=10000 + id, dport=20000 + id)) + UDP(sport=10000 + id, dport=20000 + id)) - def generatePackets(self, isv4): + def generatePackets(self, src_if, isv4): pkts = [] for pktid in self.packets: - info = self.create_packet_info(0, pktid) + info = self.create_packet_info(src_if.sw_if_index, pktid) payload = self.info_to_payload(info) ip = self.getIPv4Flow(pktid) if isv4 else self.getIPv6Flow(pktid) - packet=(Ether(dst=self.VPP_MACS[0], src=self.MY_MACS[0]) / - ip / Raw(payload)) + packet = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + ip / + Raw(payload)) self.extend_packet(packet, 128) info.data = packet.copy() pkts.append(packet) return pkts def checkInner(self, gre, isv4): + IPver = IP if isv4 else IPv6 self.assertEqual(gre.proto, 0x0800 if isv4 else 0x86DD) self.assertEqual(gre.flags, 0) self.assertEqual(gre.version, 0) - inner = gre[IP] if isv4 else gre[IPv6] - payload_info = self.payload_to_info(str(gre[Raw])) + inner = IPver(str(gre.payload)) + payload_info = self.payload_to_info(str(inner[Raw])) packet_index = payload_info.index - self.info = self.get_next_packet_info_for_interface2(0, payload_info.dst, self.info) - self.assertEqual(str(inner), str(self.info.data[IP])) + self.info = self.get_next_packet_info_for_interface2(self.pg0.sw_if_index, + payload_info.dst, + self.info) + self.assertEqual(str(inner), str(self.info.data[IPver])) def checkCapture(self, gre4, isv4): - out = self.pg_get_capture(0) - self.assertEqual(len(out), 0) - out = self.pg_get_capture(1) + out = self.pg0.get_capture() + # This check is edited because RA appears in output, maybe disable RA? + # self.assertEqual(len(out), 0) + self.assertLess(len(out), 20) + out = self.pg1.get_capture() self.assertEqual(len(out), len(self.packets)) load = [0] * len(self.ass) @@ -97,8 +112,6 @@ class TestLB(Util, VppTestCase): gre = None if gre4: ip = p[IP] - gre = p[GRE] - inner = gre[IP] if isv4 else gre[IPv6] asid = int(ip.dst.split(".")[3]) self.assertEqual(ip.version, 4) self.assertEqual(ip.flags, 0) @@ -106,105 +119,108 @@ class TestLB(Util, VppTestCase): self.assertEqual(ip.dst, "10.0.0.%u" % asid) self.assertEqual(ip.proto, 47) self.assertEqual(len(ip.options), 0) - self.assertTrue(ip.ttl >= 64) + self.assertGreaterEqual(ip.ttl, 64) + gre = p[GRE] else: ip = p[IPv6] - gre = p[GRE] - inner = gre[IP] if isv4 else gre[IPv6] asid = ip.dst.split(":") asid = asid[len(asid) - 1] - asid = 0 if asid=="" else int(asid) + asid = 0 if asid == "" else int(asid) self.assertEqual(ip.version, 6) - # Todo: Given scapy... I will do that when it works. + self.assertEqual(ip.tc, 0) + self.assertEqual(ip.fl, 0) + self.assertEqual(ip.src, "2004::1") + self.assertEqual( + socket.inet_pton(socket.AF_INET6, ip.dst), + socket.inet_pton(socket.AF_INET6, "2002::%u" % asid) + ) + self.assertEqual(ip.nh, 47) + self.assertGreaterEqual(ip.hlim, 64) + # self.assertEqual(len(ip.options), 0) + gre = GRE(str(p[IPv6].payload)) self.checkInner(gre, isv4) load[asid] += 1 except: - self.log("Unexpected or invalid packet:") + error("Unexpected or invalid packet:") p.show() raise # This is just to roughly check that the balancing algorithm # is not completly biased. for asid in self.ass: - if load[asid] < len(self.packets)/(len(self.ass)*2): - self.log("ASS is not balanced: load[%d] = %d" % (asid, load[asid])) + if load[asid] < len(self.packets) / (len(self.ass) * 2): + self.log( + "ASS is not balanced: load[%d] = %d" % (asid, load[asid])) raise Exception("Load Balancer algorithm is biased") - def test_lb_ip4_gre4(self): """ Load Balancer IP4 GRE4 """ + try: + self.vapi.cli("lb vip 90.0.0.0/8 encap gre4") + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 10.0.0.%u" % (asid)) - return - self.cli(0, "lb vip 90.0.0.0/8 encap gre4") - for asid in self.ass: - self.cli(0, "lb as 90.0.0.0/8 10.0.0.%u" % (asid)) - - self.pg_add_stream(0, self.generatePackets(1)) - self.pg_enable_capture([0,1]) - self.pg_start() - self.checkCapture(1, 1) - - for asid in self.ass: - self.cli(0, "lb as 90.0.0.0/8 10.0.0.%u del" % (asid)) - self.cli(0, "lb vip 90.0.0.0/8 encap gre4 del") + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=True)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.checkCapture(gre4=True, isv4=True) + finally: + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 10.0.0.%u del" % (asid)) + self.vapi.cli("lb vip 90.0.0.0/8 encap gre4 del") def test_lb_ip6_gre4(self): """ Load Balancer IP6 GRE4 """ - self.cli(0, "lb vip 2001::/16 encap gre4") - for asid in self.ass: - self.cli(0, "lb as 2001::/16 10.0.0.%u" % (asid)) - - self.pg_add_stream(0, self.generatePackets(0)) - self.pg_enable_capture([0,1]) - self.pg_start() - - # Scapy fails parsing IPv6 over GRE. - # This check is therefore disabled for now. - #self.checkCapture(1, 0) + try: + self.vapi.cli("lb vip 2001::/16 encap gre4") + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 10.0.0.%u" % (asid)) - for asid in self.ass: - self.cli(0, "lb as 2001::/16 10.0.0.%u del" % (asid)) - self.cli(0, "lb vip 2001::/16 encap gre4 del") + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=False)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.checkCapture(gre4=True, isv4=False) + finally: + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 10.0.0.%u del" % (asid)) + self.vapi.cli("lb vip 2001::/16 encap gre4 del") def test_lb_ip4_gre6(self): """ Load Balancer IP4 GRE6 """ - - self.cli(0, "lb vip 90.0.0.0/8 encap gre6") - for asid in self.ass: - self.cli(0, "lb as 90.0.0.0/8 2002::%u" % (asid)) - - self.pg_add_stream(0, self.generatePackets(1)) - self.pg_enable_capture([0,1]) - self.pg_start() - - # Scapy fails parsing GRE over IPv6. - # This check is therefore disabled for now. - # One can easily patch layers/inet6.py to fix the issue. - #self.checkCapture(0, 1) - - for asid in self.ass: - self.cli(0, "lb as 90.0.0.0/8 2002::%u" % (asid)) - self.cli(0, "lb vip 90.0.0.0/8 encap gre6 del") + try: + self.vapi.cli("lb vip 90.0.0.0/8 encap gre6") + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 2002::%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=True)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Scapy fails parsing GRE over IPv6. + # This check is therefore disabled for now. + # One can easily patch layers/inet6.py to fix the issue. + self.checkCapture(gre4=False, isv4=True) + finally: + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 2002::%u" % (asid)) + self.vapi.cli("lb vip 90.0.0.0/8 encap gre6 del") def test_lb_ip6_gre6(self): """ Load Balancer IP6 GRE6 """ - - self.cli(0, "lb vip 2001::/16 encap gre6") - for asid in self.ass: - self.cli(0, "lb as 2001::/16 2002::%u" % (asid)) - - self.pg_add_stream(0, self.generatePackets(0)) - self.pg_enable_capture([0,1]) - self.pg_start() - - # Scapy fails parsing IPv6 over GRE and IPv6 over GRE. - # This check is therefore disabled for now. - #self.checkCapture(0, 0) - - for asid in self.ass: - self.cli(0, "lb as 2001::/16 2002::%u del" % (asid)) - self.cli(0, "lb vip 2001::/16 encap gre6 del") - + try: + self.vapi.cli("lb vip 2001::/16 encap gre6") + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 2002::%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=False)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.checkCapture(gre4=False, isv4=False) + finally: + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 2002::%u del" % (asid)) + self.vapi.cli("lb vip 2001::/16 encap gre6 del") diff --git a/test/test_vxlan.py b/test/test_vxlan.py index 1db34927..cb7e7acf 100644 --- a/test/test_vxlan.py +++ b/test/test_vxlan.py @@ -1,8 +1,8 @@ #!/usr/bin/env python import unittest +from logging import * from framework import VppTestCase, VppTestRunner -from util import Util from template_bd import BridgeDomain from scapy.layers.l2 import Ether @@ -10,61 +10,49 @@ from scapy.layers.inet import IP, UDP from scapy_handlers.vxlan import VXLAN -## TestVxlan is a subclass of BridgeDomain, Util, VppTestCase classes. -# -# TestVxlan class defines VXLAN test cases for VXLAN encapsulation, -# decapsulation and VXLAN tunnel termination in L2 bridge-domain. -class TestVxlan(BridgeDomain, Util, VppTestCase): +class TestVxlan(BridgeDomain, VppTestCase): """ VXLAN Test Case """ - ## Method to initialize all parent classes. - # - # Initialize BridgeDomain objects, set documentation string for inherited - # tests and initialize VppTestCase object which must be called after - # doc strings are set. def __init__(self, *args): BridgeDomain.__init__(self) - self.test_decap.__func__.__doc__ = ' VXLAN BD decapsulation ' - self.test_encap.__func__.__doc__ = ' VXLAN BD encapsulation ' VppTestCase.__init__(self, *args) - ## Method for VXLAN encapsulate function. - # - # Encapsulate the original payload frame by adding VXLAN header with its - # UDP, IP and Ethernet fields. def encapsulate(self, pkt): - return (Ether(src=self.MY_MACS[0], dst=self.VPP_MACS[0]) / - IP(src=self.MY_IP4S[0], dst=self.VPP_IP4S[0]) / - UDP(sport=4789, dport=4789, chksum=0) / - VXLAN(vni=1) / + """ + Encapsulate the original payload frame by adding VXLAN header with its + UDP, IP and Ethernet fields + """ + return (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + VXLAN(vni=self.vni) / pkt) - ## Method for VXLAN decapsulate function. - # - # Decapsulate the original payload frame by removing VXLAN header with - # its UDP, IP and Ethernet fields. def decapsulate(self, pkt): + """ + Decapsulate the original payload frame by removing VXLAN header + """ return pkt[VXLAN].payload - ## Method for checking VXLAN encapsulation. + # Method for checking VXLAN encapsulation. # def check_encapsulation(self, pkt): # TODO: add error messages - ## Verify source MAC is VPP_MAC and destination MAC is MY_MAC resolved + # Verify source MAC is VPP_MAC and destination MAC is MY_MAC resolved # by VPP using ARP. - self.assertEqual(pkt[Ether].src, self.VPP_MACS[0]) - self.assertEqual(pkt[Ether].dst, self.MY_MACS[0]) - ## Verify VXLAN tunnel source IP is VPP_IP and destination IP is MY_IP. - self.assertEqual(pkt[IP].src, self.VPP_IP4S[0]) - self.assertEqual(pkt[IP].dst, self.MY_IP4S[0]) - ## Verify UDP destination port is VXLAN 4789, source UDP port could be + self.assertEqual(pkt[Ether].src, self.pg0.local_mac) + self.assertEqual(pkt[Ether].dst, self.pg0.remote_mac) + # Verify VXLAN tunnel source IP is VPP_IP and destination IP is MY_IP. + self.assertEqual(pkt[IP].src, self.pg0.local_ip4) + self.assertEqual(pkt[IP].dst, self.pg0.remote_ip4) + # Verify UDP destination port is VXLAN 4789, source UDP port could be # arbitrary. - self.assertEqual(pkt[UDP].dport, 4789) + self.assertEqual(pkt[UDP].dport, type(self).dport) # TODO: checksum check - ## Verify VNI, based on configuration it must be 1. - self.assertEqual(pkt[VXLAN].vni, 1) + # Verify VNI, based on configuration it must be 1. + self.assertEqual(pkt[VXLAN].vni, type(self).vni) - ## Class method to start the VXLAN test case. + # Class method to start the VXLAN test case. # Overrides setUpClass method in VppTestCase class. # Python try..except statement is used to ensure that the tear down of # the class will be executed even if exception is raised. @@ -72,31 +60,41 @@ class TestVxlan(BridgeDomain, Util, VppTestCase): @classmethod def setUpClass(cls): super(TestVxlan, cls).setUpClass() + try: - ## Create 2 pg interfaces. - cls.create_interfaces(range(2)) - ## Configure IPv4 addresses on VPP pg0. - cls.config_ip4([0]) - ## Resolve MAC address for VPP's IP address on pg0. - cls.resolve_arp([0]) - - ## Create VXLAN VTEP on VPP pg0, and put vxlan_tunnel0 and pg1 + cls.dport = 4789 + cls.vni = 1 + + # Create 2 pg interfaces. + cls.create_pg_interfaces(range(2)) + cls.pg0.admin_up() + cls.pg1.admin_up() + + # Configure IPv4 addresses on VPP pg0. + cls.pg0.config_ip4() + + # Resolve MAC address for VPP's IP address on pg0. + cls.pg0.resolve_arp() + + # Create VXLAN VTEP on VPP pg0, and put vxlan_tunnel0 and pg1 # into BD. - cls.api("vxlan_add_del_tunnel src %s dst %s vni 1" % - (cls.VPP_IP4S[0], cls.MY_IP4S[0])) - cls.api("sw_interface_set_l2_bridge vxlan_tunnel0 bd_id 1") - cls.api("sw_interface_set_l2_bridge pg1 bd_id 1") - except: - ## In case setUpClass fails run tear down. - cls.tearDownClass() + r = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.pg0.remote_ip4n, + vni=cls.vni) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, bd_id=1) + cls.vapi.sw_interface_set_l2_bridge(cls.pg1.sw_if_index, bd_id=1) + except Exception: + super(TestVxlan, cls).tearDownClass() raise - ## Method to define VPP actions before tear down of the test case. + # Method to define VPP actions before tear down of the test case. # Overrides tearDown method in VppTestCase class. # @param self The object pointer. def tearDown(self): super(TestVxlan, self).tearDown() - self.cli(2, "show bridge-domain 1 detail") + if not self.vpp_dead: + info(self.vapi.cli("show bridge-domain 1 detail")) if __name__ == '__main__': unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index c72a3965..8d7c9202 100644 --- a/test/util.py +++ b/test/util.py @@ -1,139 +1,25 @@ -## @package util -# Module with common functions that should be used by the test cases. -# -# The module provides a set of tools for setup the test environment +from logging import * -from scapy.layers.l2 import Ether, ARP -from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6NDOptSrcLLAddr +class TestHost(object): + """ Generic test host "connected" to VPP. """ -## Util class -# -# Test cases that want to use methods defined in Util class should -# inherit this class. -# -# class Example(Util, VppTestCase): -# pass -class Util(object): + @property + def mac(self): + """ MAC address """ + return self._mac - ## Class method to send ARP Request for each VPP IPv4 address in - # order to determine VPP interface MAC address to IPv4 bindings. - # - # Resolved MAC address is saved to the VPP_MACS dictionary with interface - # index as a key. ARP Request is sent from MAC in MY_MACS dictionary with - # interface index as a key. - # @param cls The class pointer. - # @param args List variable to store indices of VPP interfaces. - @classmethod - def resolve_arp(cls, args): - for i in args: - ip = cls.VPP_IP4S[i] - cls.log("Sending ARP request for %s on port %u" % (ip, i)) - arp_req = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / - ARP(op=ARP.who_has, pdst=ip, - psrc=cls.MY_IP4S[i], hwsrc=cls.MY_MACS[i])) - cls.pg_add_stream(i, arp_req) - cls.pg_enable_capture([i]) + @property + def ip4(self): + """ IPv4 address """ + return self._ip4 - cls.cli(2, "trace add pg-input 1") - cls.pg_start() - arp_reply = cls.pg_get_capture(i)[0] - if arp_reply[ARP].op == ARP.is_at: - cls.log("VPP pg%u MAC address is %s " % (i, arp_reply[ARP].hwsrc)) - cls.VPP_MACS[i] = arp_reply[ARP].hwsrc - else: - cls.log("No ARP received on port %u" % i) - cls.cli(2, "show trace") - ## @var ip - # - ## @var arp_req - # - ## @var arp_reply - # - ## @var VPP_MACS - # + @property + def ip6(self): + """ IPv6 address """ + return self._ip6 - ## Class method to send ND request for each VPP IPv6 address in - # order to determine VPP MAC address to IPv6 bindings. - # - # Resolved MAC address is saved to the VPP_MACS dictionary with interface - # index as a key. ND Request is sent from MAC in MY_MACS dictionary with - # interface index as a key. - # @param cls The class pointer. - # @param args List variable to store indices of VPP interfaces. - @classmethod - def resolve_icmpv6_nd(cls, args): - for i in args: - ip = cls.VPP_IP6S[i] - cls.log("Sending ICMPv6ND_NS request for %s on port %u" % (ip, i)) - nd_req = (Ether(dst="ff:ff:ff:ff:ff:ff", src=cls.MY_MACS[i]) / - IPv6(src=cls.MY_IP6S[i], dst=ip) / - ICMPv6ND_NS(tgt=ip) / - ICMPv6NDOptSrcLLAddr(lladdr=cls.MY_MACS[i])) - cls.pg_add_stream(i, nd_req) - cls.pg_enable_capture([i]) - - cls.cli(2, "trace add pg-input 1") - cls.pg_start() - nd_reply = cls.pg_get_capture(i)[0] - icmpv6_na = nd_reply['ICMPv6 Neighbor Discovery - Neighbor Advertisement'] - dst_ll_addr = icmpv6_na['ICMPv6 Neighbor Discovery Option - Destination Link-Layer Address'] - cls.VPP_MACS[i] = dst_ll_addr.lladdr - ## @var ip - # - ## @var nd_req - # - ## @var nd_reply - # - ## @var icmpv6_na - # - ## @var dst_ll_addr - # - ## @var VPP_MACS - # - - ## Class method to configure IPv4 addresses on VPP interfaces. - # - # Set dictionary variables MY_IP4S and VPP_IP4S to IPv4 addresses - # calculated using interface VPP interface index as a parameter. - # /24 IPv4 prefix is used, with VPP interface address host part set - # to .1 and MY address set to .2. - # Used IPv4 prefix scheme: 172.16.{VPP-interface-index}.0/24. - # @param cls The class pointer. - # @param args List variable to store indices of VPP interfaces. - @classmethod - def config_ip4(cls, args): - for i in args: - cls.MY_IP4S[i] = "172.16.%u.2" % i - cls.VPP_IP4S[i] = "172.16.%u.1" % i - cls.api("sw_interface_add_del_address pg%u %s/24" % (i, cls.VPP_IP4S[i])) - cls.log("My IPv4 address is %s" % (cls.MY_IP4S[i])) - ## @var MY_IP4S - # Dictionary variable to store host IPv4 addresses connected to packet - # generator interfaces. - ## @var VPP_IP4S - # Dictionary variable to store VPP IPv4 addresses of the packet - # generator interfaces. - - ## Class method to configure IPv6 addresses on VPP interfaces. - # - # Set dictionary variables MY_IP6S and VPP_IP6S to IPv6 addresses - # calculated using interface VPP interface index as a parameter. - # /64 IPv6 prefix is used, with VPP interface address host part set - # to ::1 and MY address set to ::2. - # Used IPv6 prefix scheme: fd10:{VPP-interface-index}::0/64. - # @param cls The class pointer. - # @param args List variable to store indices of VPP interfaces. - @classmethod - def config_ip6(cls, args): - for i in args: - cls.MY_IP6S[i] = "fd10:%u::2" % i - cls.VPP_IP6S[i] = "fd10:%u::1" % i - cls.api("sw_interface_add_del_address pg%u %s/64" % (i, cls.VPP_IP6S[i])) - cls.log("My IPv6 address is %s" % (cls.MY_IP6S[i])) - ## @var MY_IP6S - # Dictionary variable to store host IPv6 addresses connected to packet - # generator interfaces. - ## @var VPP_IP6S - # Dictionary variable to store VPP IPv6 addresses of the packet - # generator interfaces. + def __init__(self, mac=None, ip4=None, ip6=None): + self._mac = mac + self._ip4 = ip4 + self._ip6 = ip6 diff --git a/test/vpp_interface.py b/test/vpp_interface.py new file mode 100644 index 00000000..c596ab5a --- /dev/null +++ b/test/vpp_interface.py @@ -0,0 +1,240 @@ +from abc import abstractmethod, ABCMeta +import socket +from logging import info, error +from scapy.layers.l2 import Ether, ARP + +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr + + +class VppInterface(object): + """ + Generic VPP interface + """ + __metaclass__ = ABCMeta + + @property + def sw_if_index(self): + """Interface index assigned by VPP""" + return self._sw_if_index + + @property + def remote_mac(self): + """MAC-address of the remote interface "connected" to this interface""" + return self._remote_mac + + @property + def local_mac(self): + """MAC-address of the VPP interface""" + return self._local_mac + + @property + def local_ip4(self): + """Local IPv4 address on VPP interface (string)""" + return self._local_ip4 + + @property + def local_ip4n(self): + """Local IPv4 address - raw, suitable as API parameter""" + return self._local_ip4n + + @property + def remote_ip4(self): + """IPv4 address of remote peer "connected" to this interface""" + return self._remote_ip4 + + @property + def remote_ip4n(self): + """IPv4 address of remote peer - raw, suitable as API parameter""" + return self._remote_ip4n + + @property + def local_ip6(self): + """Local IPv6 address on VPP interface (string)""" + return self._local_ip6 + + @property + def local_ip6n(self): + """Local IPv6 address - raw, suitable as API parameter""" + return self._local_ip6n + + @property + def remote_ip6(self): + """IPv6 address of remote peer "connected" to this interface""" + return self._remote_ip6 + + @property + def remote_ip6n(self): + """IPv6 address of remote peer - raw, suitable as API parameter""" + return self._remote_ip6n + + @property + def name(self): + """Name of the interface""" + return self._name + + @property + def dump(self): + """Raw result of sw_interface_dump for this interface""" + return self._dump + + @property + def test(self): + """Test case creating this interface""" + return self._test + + def post_init_setup(self): + """Additional setup run after creating an interface object""" + self._remote_mac = "02:00:00:00:ff:%02x" % self.sw_if_index + + self._local_ip4 = "172.16.%u.1" % self.sw_if_index + self._local_ip4n = socket.inet_pton(socket.AF_INET, self.local_ip4) + self._remote_ip4 = "172.16.%u.2" % self.sw_if_index + self._remote_ip4n = socket.inet_pton(socket.AF_INET, self.remote_ip4) + + self._local_ip6 = "fd01:%u::1" % self.sw_if_index + self._local_ip6n = socket.inet_pton(socket.AF_INET6, self.local_ip6) + self._remote_ip6 = "fd01:%u::2" % self.sw_if_index + self._remote_ip6n = socket.inet_pton(socket.AF_INET6, self.remote_ip6) + + r = self.test.vapi.sw_interface_dump() + for intf in r: + if intf.sw_if_index == self.sw_if_index: + self._name = intf.interface_name.split(b'\0', 1)[0] + self._local_mac = ':'.join(intf.l2_address.encode('hex')[i:i + 2] + for i in range(0, 12, 2)) + self._dump = intf + break + else: + raise Exception( + "Could not find interface with sw_if_index %d " + "in interface dump %s" % + (self.sw_if_index, repr(r))) + + @abstractmethod + def __init__(self, test, index): + self._test = test + self.post_init_setup() + info("New %s, MAC=%s, remote_ip4=%s, local_ip4=%s" % + (self.__name__, self.remote_mac, self.remote_ip4, self.local_ip4)) + + def config_ip4(self): + """Configure IPv4 address on the VPP interface""" + addr = self.local_ip4n + addr_len = 24 + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, addr, addr_len) + + def config_ip6(self): + """Configure IPv6 address on the VPP interface""" + addr = self._local_ip6n + addr_len = 64 + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, addr, addr_len, is_ipv6=1) + + def disable_ipv6_ra(self): + """Configure IPv6 RA suppress on the VPP interface""" + self.test.vapi.sw_interface_ra_suppress(self.sw_if_index) + + def create_arp_req(self): + """Create ARP request applicable for this interface""" + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / + ARP(op=ARP.who_has, pdst=self.local_ip4, + psrc=self.remote_ip4, hwsrc=self.remote_mac)) + + def create_ndp_req(self): + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / + IPv6(src=self.remote_ip6, dst=self.local_ip6) / + ICMPv6ND_NS(tgt=self.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac)) + + def resolve_arp(self, pg_interface=None): + """Resolve ARP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + + """ + if pg_interface is None: + pg_interface = self + info("Sending ARP request for %s on port %s" % + (self.local_ip4, pg_interface.name)) + arp_req = self.create_arp_req() + pg_interface.add_stream(arp_req) + pg_interface.enable_capture() + self.test.pg_start() + info(self.test.vapi.cli("show trace")) + arp_reply = pg_interface.get_capture() + if arp_reply is None or len(arp_reply) == 0: + info("No ARP received on port %s" % pg_interface.name) + return + arp_reply = arp_reply[0] + # Make Dot1AD packet content recognizable to scapy + if arp_reply.type == 0x88a8: + arp_reply.type = 0x8100 + arp_reply = Ether(str(arp_reply)) + try: + if arp_reply[ARP].op == ARP.is_at: + info("VPP %s MAC address is %s " % + (self.name, arp_reply[ARP].hwsrc)) + self._local_mac = arp_reply[ARP].hwsrc + else: + info("No ARP received on port %s" % pg_interface.name) + except: + error("Unexpected response to ARP request:") + error(arp_reply.show()) + raise + + def resolve_ndp(self, pg_interface=None): + """Resolve NDP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + + """ + if pg_interface is None: + pg_interface = self + info("Sending NDP request for %s on port %s" % + (self.local_ip6, pg_interface.name)) + ndp_req = self.create_ndp_req() + pg_interface.add_stream(ndp_req) + pg_interface.enable_capture() + self.test.pg_start() + info(self.test.vapi.cli("show trace")) + ndp_reply = pg_interface.get_capture() + if ndp_reply is None or len(ndp_reply) == 0: + info("No NDP received on port %s" % pg_interface.name) + return + ndp_reply = ndp_reply[0] + # Make Dot1AD packet content recognizable to scapy + if ndp_reply.type == 0x88a8: + ndp_reply.type = 0x8100 + ndp_reply = Ether(str(ndp_reply)) + try: + ndp_na = ndp_reply[ICMPv6ND_NA] + opt = ndp_na[ICMPv6NDOptDstLLAddr] + info("VPP %s MAC address is %s " % + (self.name, opt.lladdr)) + self._local_mac = opt.lladdr + except: + error("Unexpected response to NDP request:") + error(ndp_reply.show()) + raise + + def admin_up(self): + """ Put interface ADMIN-UP """ + self.test.vapi.sw_interface_set_flags(self.sw_if_index, admin_up_down=1) + + def add_sub_if(self, sub_if): + """ + Register a sub-interface with this interface + + :param sub_if: sub-interface + + """ + if not hasattr(self, 'sub_if'): + self.sub_if = sub_if + else: + if isinstance(self.sub_if, list): + self.sub_if.append(sub_if) + else: + self.sub_if = sub_if diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py new file mode 100644 index 00000000..b83cd43f --- /dev/null +++ b/test/vpp_papi_provider.py @@ -0,0 +1,316 @@ +import vpp_papi +from logging import error +from hook import Hook + +# from vnet/vnet/mpls/mpls_types.h +MPLS_IETF_MAX_LABEL = 0xfffff +MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 + + +class VppPapiProvider(object): + """VPP-api provider using vpp-papi + + @property hook: hook object providing before and after api/cli hooks + + + """ + + def __init__(self, name, shm_prefix): + self.hook = Hook() + self.name = name + self.shm_prefix = shm_prefix + + def register_hook(self, hook): + """Replace hook registration with new hook + + :param hook: + + """ + self.hook = hook + + def connect(self): + """Connect the API to VPP""" + vpp_papi.connect(self.name, self.shm_prefix) + + def disconnect(self): + """Disconnect the API from VPP""" + vpp_papi.disconnect() + + def api(self, api_fn, api_args, expected_retval=0): + """Call API function and check it's return value + Call the appropriate hooks before and after the API call + + :param api_fn: API function to call + :param api_args: tuple of API function arguments + :param expected_retval: Expected return value (Default value = 0) + :returns: reply from the API + + """ + self.hook.before_api(api_fn.__name__, api_args) + reply = api_fn(*api_args) + if hasattr(reply, 'retval') and reply.retval != expected_retval: + msg = "API call failed, expected retval == %d, got %s" % ( + expected_retval, repr(reply)) + error(msg) + raise Exception(msg) + self.hook.after_api(api_fn.__name__, api_args) + return reply + + def cli(self, cli): + """Execute a CLI, calling the before/after hooks appropriately + + :param cli: CLI to execute + :returns: CLI output + + """ + self.hook.before_cli(cli) + cli += '\n' + r = vpp_papi.cli_inband(len(cli), cli) + self.hook.after_cli(cli) + if(hasattr(r, 'reply')): + return r.reply[0].decode().rstrip('\x00') + + def show_version(self): + """ """ + return vpp_papi.show_version() + + def pg_create_interface(self, pg_index): + """ + + :param pg_index: + + """ + return self.api(vpp_papi.pg_create_interface, (pg_index, )) + + def sw_interface_dump(self, filter=None): + """ + + :param filter: (Default value = None) + + """ + if filter is not None: + args = (1, filter) + else: + args = (0, b'') + return self.api(vpp_papi.sw_interface_dump, args) + + def sw_interface_add_del_address(self, sw_if_index, addr, addr_len, + is_ipv6=0, is_add=1, del_all=0): + """ + + :param addr: param is_ipv6: (Default value = 0) + :param sw_if_index: + :param addr_len: + :param is_ipv6: (Default value = 0) + :param is_add: (Default value = 1) + :param del_all: (Default value = 0) + + """ + return self.api(vpp_papi.sw_interface_add_del_address, + (sw_if_index, is_add, is_ipv6, del_all, addr_len, addr)) + + def sw_interface_ra_suppress(self, sw_if_index): + suppress = 1 + managed = 0 + other = 0 + ll_option = 0 + send_unicast = 0 + cease = 0 + is_no = 0 + default_router = 0 + max_interval = 0 + min_interval = 0 + lifetime = 0 + initial_count = 0 + initial_interval = 0 + async = False + return self.api(vpp_papi.sw_interface_ip6nd_ra_config, + (sw_if_index, suppress, managed, other, + ll_option, send_unicast, cease, is_no, + default_router, max_interval, min_interval, + lifetime, initial_count, initial_interval, async)) + + + def vxlan_add_del_tunnel( + self, + src_addr, + dst_addr, + is_add=1, + is_ipv6=0, + encap_vrf_id=0, + decap_next_index=0xFFFFFFFF, + vni=0): + """ + + :param dst_addr: + :param src_addr: + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param encap_vrf_id: (Default value = 0) + :param decap_next_index: (Default value = 0xFFFFFFFF) + :param vni: (Default value = 0) + + """ + return self.api(vpp_papi.vxlan_add_del_tunnel, + (is_add, is_ipv6, src_addr, dst_addr, encap_vrf_id, + decap_next_index, vni)) + + def sw_interface_set_l2_bridge(self, sw_if_index, bd_id, + shg=0, bvi=0, enable=1): + """ + + :param bd_id: + :param sw_if_index: + :param shg: (Default value = 0) + :param bvi: (Default value = 0) + :param enable: (Default value = 1) + + """ + return self.api(vpp_papi.sw_interface_set_l2_bridge, + (sw_if_index, bd_id, shg, bvi, enable)) + + def sw_interface_set_l2_xconnect(self, rx_sw_if_index, tx_sw_if_index, + enable): + """Create or delete unidirectional cross-connect from Tx interface to + Rx interface. + + :param rx_sw_if_index: Software interface index of Rx interface. + :param tx_sw_if_index: Software interface index of Tx interface. + :param enable: Create cross-connect if equal to 1, delete cross-connect + if equal to 0. + :type rx_sw_if_index: str or int + :type rx_sw_if_index: str or int + :type enable: int + + """ + return self.api(vpp_papi.sw_interface_set_l2_xconnect, + (rx_sw_if_index, tx_sw_if_index, enable)) + + def sw_interface_set_flags(self, sw_if_index, admin_up_down, + link_up_down=0, deleted=0): + """ + + :param admin_up_down: + :param sw_if_index: + :param link_up_down: (Default value = 0) + :param deleted: (Default value = 0) + + """ + return self.api(vpp_papi.sw_interface_set_flags, + (sw_if_index, admin_up_down, link_up_down, deleted)) + + def create_subif(self, sw_if_index, sub_id, outer_vlan, inner_vlan, + no_tags=0, one_tag=0, two_tags=0, dot1ad=0, exact_match=0, + default_sub=0, outer_vlan_id_any=0, inner_vlan_id_any=0): + """Create subinterface + from vpe.api: set dot1ad = 0 for dot1q, set dot1ad = 1 for dot1ad + + :param sub_id: param inner_vlan: + :param sw_if_index: + :param outer_vlan: + :param inner_vlan: + :param no_tags: (Default value = 0) + :param one_tag: (Default value = 0) + :param two_tags: (Default value = 0) + :param dot1ad: (Default value = 0) + :param exact_match: (Default value = 0) + :param default_sub: (Default value = 0) + :param outer_vlan_id_any: (Default value = 0) + :param inner_vlan_id_any: (Default value = 0) + + """ + return self.api( + vpp_papi.create_subif, + (sw_if_index, + sub_id, + no_tags, + one_tag, + two_tags, + dot1ad, + exact_match, + default_sub, + outer_vlan_id_any, + inner_vlan_id_any, + outer_vlan, + inner_vlan)) + + def create_vlan_subif(self, sw_if_index, vlan): + """ + + :param vlan: + :param sw_if_index: + + """ + return self.api(vpp_papi.create_vlan_subif, (sw_if_index, vlan)) + + def ip_add_del_route( + self, + dst_address, + dst_address_length, + next_hop_address, + next_hop_sw_if_index=0xFFFFFFFF, + table_id=0, + resolve_attempts=0, + classify_table_index=0xFFFFFFFF, + next_hop_out_label=MPLS_LABEL_INVALID, + next_hop_table_id=0, + create_vrf_if_needed=0, + resolve_if_needed=0, + is_add=1, + is_drop=0, + is_ipv6=0, + is_local=0, + is_classify=0, + is_multipath=0, + is_resolve_host=0, + is_resolve_attached=0, + not_last=0, + next_hop_weight=1): + """ + + :param dst_address_length: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param dst_address: + :param next_hop_address: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param vrf_id: (Default value = 0) + :param lookup_in_vrf: (Default value = 0) + :param resolve_attempts: (Default value = 0) + :param classify_table_index: (Default value = 0xFFFFFFFF) + :param create_vrf_if_needed: (Default value = 0) + :param resolve_if_needed: (Default value = 0) + :param is_add: (Default value = 1) + :param is_drop: (Default value = 0) + :param is_ipv6: (Default value = 0) + :param is_local: (Default value = 0) + :param is_classify: (Default value = 0) + :param is_multipath: (Default value = 0) + :param is_resolve_host: (Default value = 0) + :param is_resolve_attached: (Default value = 0) + :param not_last: (Default value = 0) + :param next_hop_weight: (Default value = 1) + + """ + return self.api( + vpp_papi.ip_add_del_route, + (next_hop_sw_if_index, + table_id, + resolve_attempts, + classify_table_index, + next_hop_out_label, + next_hop_table_id, + create_vrf_if_needed, + resolve_if_needed, + is_add, + is_drop, + is_ipv6, + is_local, + is_classify, + is_multipath, + is_resolve_host, + is_resolve_attached, + not_last, + next_hop_weight, + dst_address_length, + dst_address, + next_hop_address)) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py new file mode 100644 index 00000000..fc4080d2 --- /dev/null +++ b/test/vpp_pg_interface.py @@ -0,0 +1,99 @@ +import os +from logging import error +from scapy.utils import wrpcap, rdpcap +from vpp_interface import VppInterface + + +class VppPGInterface(VppInterface): + """ + VPP packet-generator interface + """ + + @property + def pg_index(self): + """packet-generator interface index assigned by VPP""" + return self._pg_index + + @property + def out_path(self): + """pcap file path - captured packets""" + return self._out_path + + @property + def in_path(self): + """ pcap file path - injected packets""" + return self._in_path + + @property + def capture_cli(self): + """CLI string to start capture on this interface""" + return self._capture_cli + + @property + def cap_name(self): + """capture name for this interface""" + return self._cap_name + + @property + def input_cli(self): + """CLI string to load the injected packets""" + return self._input_cli + + def post_init_setup(self): + """ Perform post-init setup for super class and add our own setup """ + super(VppPGInterface, self).post_init_setup() + self._out_path = self.test.tempdir + "/pg%u_out.pcap" % self.sw_if_index + self._in_path = self.test.tempdir + "/pg%u_in.pcap" % self.sw_if_index + self._capture_cli = "packet-generator capture pg%u pcap %s" % ( + self.pg_index, self.out_path) + self._cap_name = "pcap%u" % self.sw_if_index + self._input_cli = "packet-generator new pcap %s source pg%u name %s" % ( + self.in_path, self.pg_index, self.cap_name) + + def __init__(self, test, pg_index): + """ Create VPP packet-generator interface """ + self._pg_index = pg_index + self._test = test + r = self.test.vapi.pg_create_interface(self.pg_index) + self._sw_if_index = r.sw_if_index + self.post_init_setup() + + def enable_capture(self): + """ Enable capture on this packet-generator interface""" + try: + os.unlink(self.out_path) + except: + pass + # FIXME this should be an API, but no such exists atm + self.test.vapi.cli(self.capture_cli) + + def add_stream(self, pkts): + """ + Add a stream of packets to this packet-generator + + :param pkts: iterable packets + + """ + try: + os.remove(self.in_path) + except: + pass + wrpcap(self.in_path, pkts) + # FIXME this should be an API, but no such exists atm + self.test.vapi.cli(self.input_cli) + self.test.pg_streams.append(self.cap_name) + self.test.vapi.cli("trace add pg-input %d" % len(pkts)) + + def get_capture(self): + """ + Get captured packets + + :returns: iterable packets + """ + try: + output = rdpcap(self.out_path) + except IOError: # TODO + error("File %s does not exist, probably because no" + " packets arrived" % self.out_path) + return [] + return output diff --git a/test/vpp_sub_interface.py b/test/vpp_sub_interface.py new file mode 100644 index 00000000..cd98a68c --- /dev/null +++ b/test/vpp_sub_interface.py @@ -0,0 +1,143 @@ +from scapy.layers.l2 import Ether, Dot1Q +from abc import abstractmethod, ABCMeta +from vpp_interface import VppInterface + + +class VppSubInterface(VppInterface): + __metaclass__ = ABCMeta + + @property + def parent(self): + """Parent interface for this sub-interface""" + return self._parent + + @property + def sub_id(self): + """Sub-interface ID""" + return self._sub_id + + def __init__(self, test, parent, sub_id): + self._test = test + self._parent = parent + self._parent.add_sub_if(self) + self._sub_id = sub_id + + @abstractmethod + def create_arp_req(self): + pass + + @abstractmethod + def create_ndp_req(self): + pass + + def resolve_arp(self): + super(VppSubInterface, self).resolve_arp(self.parent) + + def resolve_ndp(self): + super(VppSubInterface, self).resolve_ndp(self.parent) + + @abstractmethod + def add_dot1_layer(self, pkt): + pass + + +class VppDot1QSubint(VppSubInterface): + + @property + def vlan(self): + """VLAN tag""" + return self._vlan + + def __init__(self, test, parent, sub_id, vlan=None): + if vlan is None: + vlan = sub_id + super(VppDot1QSubint, self).__init__(test, parent, sub_id) + self._vlan = vlan + r = self.test.vapi.create_vlan_subif(parent.sw_if_index, self.vlan) + self._sw_if_index = r.sw_if_index + self.post_init_setup() + + def create_arp_req(self): + packet = VppInterface.create_arp_req(self) + return self.add_dot1_layer(packet) + + def create_ndp_req(self): + packet = VppInterface.create_ndp_req(self) + return self.add_dot1_layer(packet) + + def add_dot1_layer(self, packet): + payload = packet.payload + packet.remove_payload() + packet.add_payload(Dot1Q(vlan=self.sub_id) / payload) + return packet + + def remove_dot1_layer(self, packet): + payload = packet.payload + self.test.instance().assertEqual(type(payload), Dot1Q) + self.test.instance().assertEqual(payload.vlan, self.vlan) + payload = payload.payload + packet.remove_payload() + packet.add_payload(payload) + return packet + + +class VppDot1ADSubint(VppSubInterface): + + @property + def outer_vlan(self): + """Outer VLAN tag""" + return self._outer_vlan + + @property + def inner_vlan(self): + """Inner VLAN tag""" + return self._inner_vlan + + def __init__(self, test, parent, sub_id, outer_vlan, inner_vlan): + super(VppDot1ADSubint, self).__init__(test, parent, sub_id) + self.DOT1AD_TYPE = 0x88A8 + self.DOT1Q_TYPE = 0x8100 + self._outer_vlan = outer_vlan + self._inner_vlan = inner_vlan + r = self.test.vapi.create_subif( + parent.sw_if_index, + self.sub_id, + self.outer_vlan, + self.inner_vlan, + dot1ad=1, + two_tags=1, + exact_match=1) + self._sw_if_index = r.sw_if_index + self.post_init_setup() + + def create_arp_req(self): + packet = VppInterface.create_arp_req(self) + return self.add_dot1_layer(packet) + + def create_ndp_req(self): + packet = VppInterface.create_ndp_req(self) + return self.add_dot1_layer(packet) + + def add_dot1_layer(self, packet): + payload = packet.payload + packet.remove_payload() + packet.add_payload(Dot1Q(vlan=self.outer_vlan) / + Dot1Q(vlan=self.inner_vlan) / payload) + packet.type = self.DOT1AD_TYPE + return packet + + def remove_dot1_layer(self, packet): + self.test.instance().assertEqual(type(packet), Ether) + self.test.instance().assertEqual(packet.type, self.DOT1AD_TYPE) + packet.type = self.DOT1Q_TYPE + packet = Ether(str(packet)) + payload = packet.payload + self.test.instance().assertEqual(type(payload), Dot1Q) + self.test.instance().assertEqual(payload.vlan, self.outer_vlan) + payload = payload.payload + self.test.instance().assertEqual(type(payload), Dot1Q) + self.test.instance().assertEqual(payload.vlan, self.inner_vlan) + payload = payload.payload + packet.remove_payload() + packet.add_payload(payload) + return packet -- cgit 1.2.3-korg From 0178d52384e0428368f1acf3163e664ecda7b64c Mon Sep 17 00:00:00 2001 From: Matej Klotton Date: Fri, 4 Nov 2016 11:11:44 +0100 Subject: Add IRB test - JIRA: CSIT-255 - create loopback interfaces - move pg-interface specific arp and neighbor discovery from vpp_interface to vpp_pg_interface - base configuration of IRB tests - IP test scenario Change-Id: I9945a188163652a4e22325877aef008c4d029557 Signed-off-by: Matej Klotton --- test/framework.py | 19 +++- test/test_ip4_irb.py | 247 ++++++++++++++++++++++++++++++++++++++++++++++ test/test_l2bd.py | 4 +- test/test_l2xc.py | 4 +- test/util.py | 12 ++- test/vpp_interface.py | 155 ++++++++++------------------- test/vpp_lo_interface.py | 16 +++ test/vpp_papi_provider.py | 39 ++++++++ test/vpp_pg_interface.py | 91 ++++++++++++++++- test/vpp_sub_interface.py | 15 +-- 10 files changed, 483 insertions(+), 119 deletions(-) create mode 100644 test/test_ip4_irb.py create mode 100644 test/vpp_lo_interface.py (limited to 'test/util.py') diff --git a/test/framework.py b/test/framework.py index b10592cf..8dbc18fd 100644 --- a/test/framework.py +++ b/test/framework.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from abc import * -import os import subprocess import unittest import tempfile @@ -13,6 +11,7 @@ from threading import Thread from inspect import getdoc from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface +from vpp_lo_interface import VppLoInterface from vpp_papi_provider import VppPapiProvider from scapy.packet import Raw from log import * @@ -330,6 +329,22 @@ class VppTestCase(unittest.TestCase): cls.pg_interfaces = result return result + @classmethod + def create_loopback_interfaces(cls, interfaces): + """ + Create loopback interfaces + + :param interfaces: iterable indexes of the interfaces + + """ + result = [] + for i in interfaces: + intf = VppLoInterface(cls, i) + setattr(cls, intf.name, intf) + result.append(intf) + cls.lo_interfaces = result + return result + @staticmethod def extend_packet(packet, size): """ diff --git a/test/test_ip4_irb.py b/test/test_ip4_irb.py new file mode 100644 index 00000000..412575db --- /dev/null +++ b/test/test_ip4_irb.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python +import unittest +from random import choice, randint + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP +from logging import * + +from framework import VppTestCase, VppTestRunner + +""" IRB Test Case + +config + L2 MAC learning enabled in l2bd + 2 routed interfaces untagged, bvi + 2 bridged interfaces in l2bd with bvi +test + sending ip4 eth pkts between routed interfaces + 2 routed interfaces + 2 bridged interfaces + 64B, 512B, 1518B, 9200B (ether_size) + burst of pkts per interface + 257pkts per burst + routed pkts hitting different FIB entries + bridged pkts hitting different MAC entries +verify + all packets received correctly +""" + + +class TestIpIrb(VppTestCase): + """ IRB Test Case """ + + @classmethod + def setUpClass(cls): + super(TestIpIrb, cls).setUpClass() + + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] # packet sizes + cls.bd_id = 10 + cls.remote_hosts_count = 250 + + # create 3 pg interfaces, 1 loopback interface + cls.create_pg_interfaces(range(3)) + cls.create_loopback_interfaces(range(1)) + + cls.interfaces = list(cls.pg_interfaces) + cls.interfaces.extend(cls.lo_interfaces) + + for i in cls.interfaces: + i.admin_up() + + # Create BD with MAC learning enabled and put interfaces to this BD + cls.vapi.sw_interface_set_l2_bridge( + cls.loop0.sw_if_index, bd_id=cls.bd_id, bvi=1) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg0.sw_if_index, bd_id=cls.bd_id) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg1.sw_if_index, bd_id=cls.bd_id) + + cls.loop0.config_ip4() + cls.pg2.config_ip4() + + # configure MAC address binding to IPv4 neighbors on loop0 + cls.loop0.generate_remote_hosts(cls.remote_hosts_count) + cls.loop0.configure_extend_ipv4_mac_binding() + # configure MAC address on pg2 + cls.pg2.resolve_arp() + + # one half of hosts are behind pg0 second behind pg1 + half = cls.remote_hosts_count // 2 + cls.pg0.remote_hosts = cls.loop0.remote_hosts[:half] + cls.pg1.remote_hosts = cls.loop0.remote_hosts[half:] + + def tearDown(self): + super(TestIpIrb, self).tearDown() + if not self.vpp_dead: + info(self.vapi.cli("show l2patch")) + info(self.vapi.cli("show l2fib verbose")) + info(self.vapi.cli("show bridge-domain %s detail" % self.bd_id)) + info(self.vapi.cli("show ip arp")) + + def create_stream(self, src_ip_if, dst_ip_if, packet_sizes): + pkts = [] + for i in range(0, 257): + remote_dst_host = choice(dst_ip_if.remote_hosts) + info = self.create_packet_info( + src_ip_if.sw_if_index, dst_ip_if.sw_if_index) + payload = self.info_to_payload(info) + p = (Ether(dst=src_ip_if.local_mac, src=src_ip_if.remote_mac) / + IP(src=src_ip_if.remote_ip4, + dst=remote_dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def create_stream_l2_to_ip(self, src_l2_if, src_ip_if, dst_ip_if, + packet_sizes): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info( + src_ip_if.sw_if_index, dst_ip_if.sw_if_index) + payload = self.info_to_payload(info) + + host = choice(src_l2_if.remote_hosts) + + p = (Ether(src=host.mac, + dst = src_ip_if.local_mac) / + IP(src=host.ip4, + dst=dst_ip_if.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + + info.data = p.copy() + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + + pkts.append(p) + return pkts + + def verify_capture_l2_to_ip(self, dst_ip_if, src_ip_if, capture): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + + dst_ip_sw_if_index = dst_ip_if.sw_if_index + + for packet in capture: + ip = packet[IP] + udp = packet[IP][UDP] + payload_info = self.payload_to_info(str(packet[IP][UDP][Raw])) + packet_index = payload_info.index + + self.assertEqual(payload_info.dst, dst_ip_sw_if_index) + + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_ip_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + saved_packet = next_info.data + self.assertTrue(next_info is not None) + + # MAC: src, dst + self.assertEqual(packet.src, dst_ip_if.local_mac) + self.assertEqual(packet.dst, dst_ip_if.remote_mac) + + # IP: src, dst + host = src_ip_if.host_by_ip4(ip.src) + self.assertIsNotNone(host) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(ip.dst, dst_ip_if.remote_ip4) + + # UDP: + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + + def verify_capture(self, dst_ip_if, src_ip_if, capture): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + + dst_ip_sw_if_index = dst_ip_if.sw_if_index + + for packet in capture: + ip = packet[IP] + udp = packet[IP][UDP] + payload_info = self.payload_to_info(str(packet[IP][UDP][Raw])) + packet_index = payload_info.index + + self.assertEqual(payload_info.dst, dst_ip_sw_if_index) + + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_ip_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + self.assertTrue(next_info is not None) + + # MAC: src, dst + self.assertEqual(packet.src, dst_ip_if.local_mac) + host = dst_ip_if.host_by_mac(packet.dst) + + # IP: src, dst + self.assertEqual(ip.src, src_ip_if.remote_ip4) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(ip.dst, host.ip4) + + # UDP: + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + + def test_ip4_irb_1(self): + """ IPv4 IRB test 1 + + Test scenario: + ip traffic from pg2 interface must ends in both pg0 and pg1 + - arp entry present in loop0 interface for dst IP + - no l2 entree configured, pg0 and pg1 are same + """ + + stream = self.create_stream( + self.pg2, self.loop0, self.pg_if_packet_sizes) + self.pg2.add_stream(stream) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rcvd1 = self.pg0.get_capture() + rcvd2 = self.pg1.get_capture() + + self.verify_capture(self.loop0, self.pg2, rcvd1) + self.verify_capture(self.loop0, self.pg2, rcvd2) + + self.assertListEqual(rcvd1.res, rcvd2.res) + + def test_ip4_irb_2(self): + """ IPv4 IRB test 2 + + Test scenario: + ip traffic from pg0 and pg1 ends on pg2 + """ + + stream1 = self.create_stream_l2_to_ip( + self.pg0, self.loop0, self.pg2, self.pg_if_packet_sizes) + stream2 = self.create_stream_l2_to_ip( + self.pg1, self.loop0, self.pg2, self.pg_if_packet_sizes) + self.pg0.add_stream(stream1) + self.pg1.add_stream(stream2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rcvd = self.pg2.get_capture() + self.verify_capture_l2_to_ip(self.pg2, self.loop0, rcvd) + + self.assertEqual(len(stream1) + len(stream2), len(rcvd.res)) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2bd.py b/test/test_l2bd.py index 53e1ee05..3c65cc1e 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -10,7 +10,7 @@ from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner from vpp_sub_interface import VppDot1QSubint -from util import TestHost +from util import Host class TestL2bd(VppTestCase): @@ -100,7 +100,7 @@ class TestL2bd(VppTestCase): hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] packets = [] for j in range(start_nr, end_nr): - host = TestHost( + host = Host( "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac)) diff --git a/test/test_l2xc.py b/test/test_l2xc.py index 35c7a818..23fd7577 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -9,7 +9,7 @@ from scapy.layers.inet import IP, UDP from logging import * from framework import VppTestCase, VppTestRunner -from util import TestHost +from util import Host class TestL2xc(VppTestCase): @@ -85,7 +85,7 @@ class TestL2xc(VppTestCase): self.hosts_by_pg_idx[pg_if.sw_if_index] = [] hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] for j in range(0, count): - host = TestHost( + host = Host( "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) hosts.append(host) diff --git a/test/util.py b/test/util.py index 8d7c9202..6e7e275c 100644 --- a/test/util.py +++ b/test/util.py @@ -1,8 +1,7 @@ -from logging import * +import socket - -class TestHost(object): - """ Generic test host "connected" to VPP. """ +class Host(object): + """ Generic test host "connected" to VPPs interface. """ @property def mac(self): @@ -14,6 +13,11 @@ class TestHost(object): """ IPv4 address """ return self._ip4 + @property + def ip4n(self): + """ IPv4 address """ + return socket.inet_pton(socket.AF_INET, self._ip4) + @property def ip6(self): """ IPv6 address """ diff --git a/test/vpp_interface.py b/test/vpp_interface.py index 509ab952..d74248a3 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -1,9 +1,8 @@ from abc import abstractmethod, ABCMeta import socket -from logging import info, error -from scapy.layers.l2 import Ether, ARP +from logging import info -from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr +from util import Host class VppInterface(object): @@ -20,7 +19,7 @@ class VppInterface(object): @property def remote_mac(self): """MAC-address of the remote interface "connected" to this interface""" - return self._remote_mac + return self._remote_hosts[0].mac @property def local_mac(self): @@ -35,17 +34,17 @@ class VppInterface(object): @property def local_ip4n(self): """Local IPv4 address - raw, suitable as API parameter""" - return self._local_ip4n + return socket.inet_pton(socket.AF_INET, self._local_ip4) @property def remote_ip4(self): """IPv4 address of remote peer "connected" to this interface""" - return self._remote_ip4 + return self._remote_hosts[0].ip4 @property def remote_ip4n(self): """IPv4 address of remote peer - raw, suitable as API parameter""" - return self._remote_ip4n + return socket.inet_pton(socket.AF_INET, self.remote_ip4) @property def local_ip6(self): @@ -55,17 +54,17 @@ class VppInterface(object): @property def local_ip6n(self): """Local IPv6 address - raw, suitable as API parameter""" - return self._local_ip6n + return socket.inet_pton(socket.AF_INET6, self.local_ip6) @property def remote_ip6(self): """IPv6 address of remote peer "connected" to this interface""" - return self._remote_ip6 + return self._remote_hosts[0].ip6 @property def remote_ip6n(self): """IPv6 address of remote peer - raw, suitable as API parameter""" - return self._remote_ip6n + return socket.inet_pton(socket.AF_INET6, self.remote_ip6) @property def name(self): @@ -82,19 +81,51 @@ class VppInterface(object): """Test case creating this interface""" return self._test + @property + def remote_hosts(self): + """Remote hosts list""" + return self._remote_hosts + + @remote_hosts.setter + def remote_hosts(self, value): + self._remote_hosts = value + #TODO: set hosts_by dicts + + def host_by_mac(self, mac): + return self._hosts_by_mac[mac] + + def host_by_ip4(self, ip): + return self._hosts_by_ip4[ip] + + def host_by_ip6(self, ip): + return self._hosts_by_ip6[ip] + + def generate_remote_hosts(self, count=1): + """Generate and add remote hosts for the interface.""" + self._remote_hosts = [] + self._hosts_by_mac = {} + self._hosts_by_ip4 = {} + self._hosts_by_ip6 = {} + for i in range(2, count+2): # 0: network address, 1: local vpp address + mac = "02:%02x:00:00:ff:%02x" % (self.sw_if_index, i) + ip4 = "172.16.%u.%u" % (self.sw_if_index, i) + ip6 = "fd01:%04x::%04x" % (self.sw_if_index, i) + host = Host(mac, ip4, ip6) + self._remote_hosts.append(host) + self._hosts_by_mac[mac] = host + self._hosts_by_ip4[ip4] = host + self._hosts_by_ip6[ip6] = host + def post_init_setup(self): """Additional setup run after creating an interface object""" - self._remote_mac = "02:00:00:00:ff:%02x" % self.sw_if_index + + self.generate_remote_hosts() self._local_ip4 = "172.16.%u.1" % self.sw_if_index self._local_ip4n = socket.inet_pton(socket.AF_INET, self.local_ip4) - self._remote_ip4 = "172.16.%u.2" % self.sw_if_index - self._remote_ip4n = socket.inet_pton(socket.AF_INET, self.remote_ip4) - self._local_ip6 = "fd01:%u::1" % self.sw_if_index + self._local_ip6 = "fd01:%04x::1" % self.sw_if_index self._local_ip6n = socket.inet_pton(socket.AF_INET6, self.local_ip6) - self._remote_ip6 = "fd01:%u::2" % self.sw_if_index - self._remote_ip6n = socket.inet_pton(socket.AF_INET6, self.remote_ip6) r = self.test.vapi.sw_interface_dump() for intf in r: @@ -124,6 +155,13 @@ class VppInterface(object): self.test.vapi.sw_interface_add_del_address( self.sw_if_index, addr, addr_len) + def configure_extend_ipv4_mac_binding(self): + """Configure neighbor MAC to IPv4 addresses.""" + for host in self._remote_hosts: + macn = host.mac.replace(":", "").decode('hex') + ipn = host.ip4n + self.test.vapi.ip_neighbor_add_del(self.sw_if_index, macn, ipn) + def config_ip6(self): """Configure IPv6 address on the VPP interface""" addr = self._local_ip6n @@ -147,91 +185,6 @@ class VppInterface(object): """Configure IPv6 RA suppress on the VPP interface""" self.test.vapi.sw_interface_ra_suppress(self.sw_if_index) - def create_arp_req(self): - """Create ARP request applicable for this interface""" - return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / - ARP(op=ARP.who_has, pdst=self.local_ip4, - psrc=self.remote_ip4, hwsrc=self.remote_mac)) - - def create_ndp_req(self): - return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / - IPv6(src=self.remote_ip6, dst=self.local_ip6) / - ICMPv6ND_NS(tgt=self.local_ip6) / - ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac)) - - def resolve_arp(self, pg_interface=None): - """Resolve ARP using provided packet-generator interface - - :param pg_interface: interface used to resolve, if None then this - interface is used - - """ - if pg_interface is None: - pg_interface = self - info("Sending ARP request for %s on port %s" % - (self.local_ip4, pg_interface.name)) - arp_req = self.create_arp_req() - pg_interface.add_stream(arp_req) - pg_interface.enable_capture() - self.test.pg_start() - info(self.test.vapi.cli("show trace")) - arp_reply = pg_interface.get_capture() - if arp_reply is None or len(arp_reply) == 0: - info("No ARP received on port %s" % pg_interface.name) - return - arp_reply = arp_reply[0] - # Make Dot1AD packet content recognizable to scapy - if arp_reply.type == 0x88a8: - arp_reply.type = 0x8100 - arp_reply = Ether(str(arp_reply)) - try: - if arp_reply[ARP].op == ARP.is_at: - info("VPP %s MAC address is %s " % - (self.name, arp_reply[ARP].hwsrc)) - self._local_mac = arp_reply[ARP].hwsrc - else: - info("No ARP received on port %s" % pg_interface.name) - except: - error("Unexpected response to ARP request:") - error(arp_reply.show()) - raise - - def resolve_ndp(self, pg_interface=None): - """Resolve NDP using provided packet-generator interface - - :param pg_interface: interface used to resolve, if None then this - interface is used - - """ - if pg_interface is None: - pg_interface = self - info("Sending NDP request for %s on port %s" % - (self.local_ip6, pg_interface.name)) - ndp_req = self.create_ndp_req() - pg_interface.add_stream(ndp_req) - pg_interface.enable_capture() - self.test.pg_start() - info(self.test.vapi.cli("show trace")) - ndp_reply = pg_interface.get_capture() - if ndp_reply is None or len(ndp_reply) == 0: - info("No NDP received on port %s" % pg_interface.name) - return - ndp_reply = ndp_reply[0] - # Make Dot1AD packet content recognizable to scapy - if ndp_reply.type == 0x88a8: - ndp_reply.type = 0x8100 - ndp_reply = Ether(str(ndp_reply)) - try: - ndp_na = ndp_reply[ICMPv6ND_NA] - opt = ndp_na[ICMPv6NDOptDstLLAddr] - info("VPP %s MAC address is %s " % - (self.name, opt.lladdr)) - self._local_mac = opt.lladdr - except: - error("Unexpected response to NDP request:") - error(ndp_reply.show()) - raise - def admin_up(self): """ Put interface ADMIN-UP """ self.test.vapi.sw_interface_set_flags(self.sw_if_index, admin_up_down=1) diff --git a/test/vpp_lo_interface.py b/test/vpp_lo_interface.py new file mode 100644 index 00000000..ed9ac725 --- /dev/null +++ b/test/vpp_lo_interface.py @@ -0,0 +1,16 @@ + +from vpp_interface import VppInterface + + +class VppLoInterface(VppInterface): + """ + VPP loopback interface + """ + + def __init__(self, test, lo_index): + """ Create VPP loopback interface """ + self._lo_index = lo_index + self._test = test + r = self.test.vapi.create_loopback() + self._sw_if_index = r.sw_if_index + self.post_init_setup() diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index f0eb410b..10445de6 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -277,6 +277,13 @@ class VppPapiProvider(object): """ return self.api(vpp_papi.create_vlan_subif, (sw_if_index, vlan)) + def create_loopback(self, mac=''): + """ + + :param mac: (Optional) + """ + return self.api(vpp_papi.create_loopback, (mac,)) + def ip_add_del_route( self, dst_address, @@ -352,3 +359,35 @@ class VppPapiProvider(object): dst_address_length, dst_address, next_hop_address)) + + def ip_neighbor_add_del(self, + sw_if_index, + mac_address, + dst_address, + vrf_id=0, + is_add=1, + is_ipv6=0, + is_static=0, + ): + """ Add neighbor MAC to IPv4 or IPv6 address. + + :param sw_if_index: + :param mac_address: + :param dst_address: + :param vrf_id: (Default value = 0) + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param is_static: (Default value = 0) + """ + + return self.api( + vpp_papi.ip_neighbor_add_del, + (vrf_id, + sw_if_index, + is_add, + is_ipv6, + is_static, + mac_address, + dst_address + ) + ) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index c9e4076d..81c7192e 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,9 +1,12 @@ import os import time -from logging import error +from logging import error, info from scapy.utils import wrpcap, rdpcap from vpp_interface import VppInterface +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr + class VppPGInterface(VppInterface): """ @@ -130,3 +133,89 @@ class VppPGInterface(VppInterface): " packets arrived" % self.out_path) return [] return output + + def create_arp_req(self): + """Create ARP request applicable for this interface""" + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / + ARP(op=ARP.who_has, pdst=self.local_ip4, + psrc=self.remote_ip4, hwsrc=self.remote_mac)) + + def create_ndp_req(self): + """Create NDP - NS applicable for this interface""" + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / + IPv6(src=self.remote_ip6, dst=self.local_ip6) / + ICMPv6ND_NS(tgt=self.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac)) + + def resolve_arp(self, pg_interface=None): + """Resolve ARP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + + """ + if pg_interface is None: + pg_interface = self + info("Sending ARP request for %s on port %s" % + (self.local_ip4, pg_interface.name)) + arp_req = self.create_arp_req() + pg_interface.add_stream(arp_req) + pg_interface.enable_capture() + self.test.pg_start() + info(self.test.vapi.cli("show trace")) + arp_reply = pg_interface.get_capture() + if arp_reply is None or len(arp_reply) == 0: + info("No ARP received on port %s" % pg_interface.name) + return + arp_reply = arp_reply[0] + # Make Dot1AD packet content recognizable to scapy + if arp_reply.type == 0x88a8: + arp_reply.type = 0x8100 + arp_reply = Ether(str(arp_reply)) + try: + if arp_reply[ARP].op == ARP.is_at: + info("VPP %s MAC address is %s " % + (self.name, arp_reply[ARP].hwsrc)) + self._local_mac = arp_reply[ARP].hwsrc + else: + info("No ARP received on port %s" % pg_interface.name) + except: + error("Unexpected response to ARP request:") + error(arp_reply.show()) + raise + + def resolve_ndp(self, pg_interface=None): + """Resolve NDP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + + """ + if pg_interface is None: + pg_interface = self + info("Sending NDP request for %s on port %s" % + (self.local_ip6, pg_interface.name)) + ndp_req = self.create_ndp_req() + pg_interface.add_stream(ndp_req) + pg_interface.enable_capture() + self.test.pg_start() + info(self.test.vapi.cli("show trace")) + ndp_reply = pg_interface.get_capture() + if ndp_reply is None or len(ndp_reply) == 0: + info("No NDP received on port %s" % pg_interface.name) + return + ndp_reply = ndp_reply[0] + # Make Dot1AD packet content recognizable to scapy + if ndp_reply.type == 0x88a8: + ndp_reply.type = 0x8100 + ndp_reply = Ether(str(ndp_reply)) + try: + ndp_na = ndp_reply[ICMPv6ND_NA] + opt = ndp_na[ICMPv6NDOptDstLLAddr] + info("VPP %s MAC address is %s " % + (self.name, opt.lladdr)) + self._local_mac = opt.lladdr + except: + error("Unexpected response to NDP request:") + error(ndp_reply.show()) + raise diff --git a/test/vpp_sub_interface.py b/test/vpp_sub_interface.py index cd98a68c..b387d27b 100644 --- a/test/vpp_sub_interface.py +++ b/test/vpp_sub_interface.py @@ -1,9 +1,10 @@ from scapy.layers.l2 import Ether, Dot1Q from abc import abstractmethod, ABCMeta from vpp_interface import VppInterface +from vpp_pg_interface import VppPGInterface -class VppSubInterface(VppInterface): +class VppSubInterface(VppPGInterface): __metaclass__ = ABCMeta @property @@ -55,14 +56,14 @@ class VppDot1QSubint(VppSubInterface): self._vlan = vlan r = self.test.vapi.create_vlan_subif(parent.sw_if_index, self.vlan) self._sw_if_index = r.sw_if_index - self.post_init_setup() + VppInterface.post_init_setup(self) def create_arp_req(self): - packet = VppInterface.create_arp_req(self) + packet = VppPGInterface.create_arp_req(self) return self.add_dot1_layer(packet) def create_ndp_req(self): - packet = VppInterface.create_ndp_req(self) + packet = VppPGInterface.create_ndp_req(self) return self.add_dot1_layer(packet) def add_dot1_layer(self, packet): @@ -108,14 +109,14 @@ class VppDot1ADSubint(VppSubInterface): two_tags=1, exact_match=1) self._sw_if_index = r.sw_if_index - self.post_init_setup() + VppInterface.post_init_setup(self) def create_arp_req(self): - packet = VppInterface.create_arp_req(self) + packet = VppPGInterface.create_arp_req(self) return self.add_dot1_layer(packet) def create_ndp_req(self): - packet = VppInterface.create_ndp_req(self) + packet = VppPGInterface.create_ndp_req(self) return self.add_dot1_layer(packet) def add_dot1_layer(self, packet): -- cgit 1.2.3-korg From 7bb873a4cc068a6cc3c9d0e1d32987c5f8003904 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Fri, 18 Nov 2016 07:38:42 +0100 Subject: make test: fix missing log/packet messages Change-Id: Idb3119792943664748c4abc3829ad723f4156dfe Signed-off-by: Klement Sekera --- test/framework.py | 2 +- test/test_ip4.py | 12 +++++------- test/test_ip6.py | 13 ++++++------- test/test_l2bd.py | 7 +++---- test/test_l2xc.py | 5 ++--- test/test_lb.py | 7 +++---- test/test_mpls.py | 12 ++++++------ test/test_span.py | 49 +++++++++++++++++++++++++++-------------------- test/test_vxlan.py | 3 +-- test/util.py | 14 ++++++++++++++ test/vpp_interface.py | 12 +++++------- test/vpp_papi_provider.py | 11 +++++++---- test/vpp_pg_interface.py | 46 +++++++++++++++++++++++++------------------- 13 files changed, 107 insertions(+), 86 deletions(-) (limited to 'test/util.py') diff --git a/test/framework.py b/test/framework.py index 5a9aba2c..b3cbb08a 100644 --- a/test/framework.py +++ b/test/framework.py @@ -193,7 +193,7 @@ class VppTestCase(unittest.TestCase): cls.vpp_stderr_reader_thread = Thread(target=pump_output, args=( cls.vpp.stderr, cls.vpp_stderr_queue)) cls.vpp_stderr_reader_thread.start() - cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix) + cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls) if cls.step: hook = StepHook(cls) else: diff --git a/test/test_ip4.py b/test/test_ip4.py index 36a907a6..d219ec9f 100644 --- a/test/test_ip4.py +++ b/test/test_ip4.py @@ -4,12 +4,12 @@ import unittest import socket from framework import VppTestCase, VppTestRunner -from vpp_interface import VppInterface from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint from scapy.packet import Raw from scapy.layers.l2 import Ether, Dot1Q from scapy.layers.inet import IP, UDP +from util import ppp class TestIPv4(VppTestCase): @@ -164,16 +164,14 @@ class TestIPv4(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.interfaces: remaining_packet = self.get_next_packet_info_for_interface2( i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) - self.assertTrue( - remaining_packet is None, - "Interface %s: Packet expected from interface %s didn't arrive" % - (dst_if.name, i.name)) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) def test_fib(self): """ IPv4 FIB test diff --git a/test/test_ip6.py b/test/test_ip6.py index bff829b7..06b15f94 100644 --- a/test/test_ip6.py +++ b/test/test_ip6.py @@ -9,6 +9,7 @@ from vpp_sub_interface import VppSubInterface, VppDot1QSubint from scapy.packet import Raw from scapy.layers.l2 import Ether, Dot1Q from scapy.layers.inet6 import IPv6, UDP +from util import ppp class TestIPv6(VppTestCase): @@ -103,7 +104,7 @@ class TestIPv6(VppTestCase): counter += 1 if counter / count * 100 > percent: self.logger.info("Configure %d FIB entries .. %d%% done" % - (count, percent)) + (count, percent)) percent += 1 def create_stream(self, src_if, packet_sizes): @@ -171,16 +172,14 @@ class TestIPv6(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.interfaces: remaining_packet = self.get_next_packet_info_for_interface2( i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) - self.assertTrue( - remaining_packet is None, - "Interface %s: Packet expected from interface %s didn't arrive" % - (dst_if.name, i.name)) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) def test_fib(self): """ IPv6 FIB test diff --git a/test/test_l2bd.py b/test/test_l2bd.py index 46ba2e49..50720e64 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -8,7 +8,7 @@ from scapy.layers.l2 import Ether, Dot1Q from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp from vpp_sub_interface import VppDot1QSubint, VppDot1ADSubint @@ -109,7 +109,7 @@ class TestL2bd(VppTestCase): if not self.vpp_dead: self.logger.info(self.vapi.ppcli("show l2fib verbose")) self.logger.info(self.vapi.ppcli("show bridge-domain %s detail" % - self.bd_id)) + self.bd_id)) @classmethod def create_hosts_and_learn(cls, count): @@ -217,8 +217,7 @@ class TestL2bd(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.pg_interfaces: remaining_packet = self.get_next_packet_info_for_interface2( diff --git a/test/test_l2xc.py b/test/test_l2xc.py index 49ca9968..37893042 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -8,7 +8,7 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp class TestL2xc(VppTestCase): @@ -169,8 +169,7 @@ class TestL2xc(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.interfaces: remaining_packet = self.get_next_packet_info_for_interface2( diff --git a/test/test_lb.py b/test/test_lb.py index fa4900d2..3e7f5e13 100644 --- a/test/test_lb.py +++ b/test/test_lb.py @@ -1,5 +1,4 @@ import socket -from logging import * from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 @@ -7,6 +6,7 @@ from scapy.layers.l2 import Ether, GRE from scapy.packet import Raw from framework import VppTestCase +from util import ppp """ TestLB is a subclass of VPPTestCase classes. @@ -57,7 +57,7 @@ class TestLB(VppTestCase): def tearDown(self): super(TestLB, self).tearDown() if not self.vpp_dead: - info(self.vapi.cli("show lb vip verbose")) + self.logger.info(self.vapi.cli("show lb vip verbose")) def getIPv4Flow(self, id): return (IP(dst="90.0.%u.%u" % (id / 255, id % 255), @@ -139,8 +139,7 @@ class TestLB(VppTestCase): self.checkInner(gre, isv4) load[asid] += 1 except: - error("Unexpected or invalid packet:") - p.show() + self.logger.error(ppp("Unexpected or invalid packet:", p)) raise # This is just to roughly check that the balancing algorithm diff --git a/test/test_mpls.py b/test/test_mpls.py index d1b1b919..08cd55b7 100644 --- a/test/test_mpls.py +++ b/test/test_mpls.py @@ -1,18 +1,18 @@ #!/usr/bin/env python import unittest -import socket -from logging import * from framework import VppTestCase, VppTestRunner from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint from vpp_ip_route import IpRoute, RoutePath, MplsRoute, MplsIpBind from scapy.packet import Raw -from scapy.layers.l2 import Ether, Dot1Q, ARP +from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP -from scapy.layers.inet6 import ICMPv6ND_NS, IPv6, UDP +from scapy.layers.inet6 import IPv6 from scapy.contrib.mpls import MPLS +from util import ppp + class TestMPLS(VppTestCase): @@ -621,8 +621,8 @@ class TestMPLS(VppTestCase): try: self.assertEqual(0, len(rx)) except: - error("MPLS TTL=0 packets forwarded") - error(packet.show()) + self.logger.error("MPLS TTL=0 packets forwarded") + self.logger.error(ppp("", rx)) raise # diff --git a/test/test_span.py b/test/test_span.py index 59ef5efc..e42fbd77 100644 --- a/test/test_span.py +++ b/test/test_span.py @@ -1,15 +1,13 @@ #!/usr/bin/env python import unittest -import random from scapy.packet import Raw from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP -from logging import * from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp class TestSpan(VppTestCase): @@ -34,7 +32,7 @@ class TestSpan(VppTestCase): self.flows[self.pg0] = [self.pg1] # packet sizes - self.pg_if_packet_sizes = [64, 512] #, 1518, 9018] + self.pg_if_packet_sizes = [64, 512] # , 1518, 9018] self.interfaces = list(self.pg_interfaces) @@ -56,7 +54,8 @@ class TestSpan(VppTestCase): i.resolve_arp() # Enable SPAN on pg0 (mirrored to pg2) - self.vapi.sw_interface_span_enable_disable(self.pg0.sw_if_index, self.pg2.sw_if_index) + self.vapi.sw_interface_span_enable_disable( + self.pg0.sw_if_index, self.pg2.sw_if_index) def tearDown(self): super(TestSpan, self).tearDown() @@ -86,8 +85,6 @@ class TestSpan(VppTestCase): pkts = [] for i in range(0, TestSpan.pkts_per_burst): dst_if = self.flows[src_if][0] - dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) - src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) pkt_info = self.create_packet_info( src_if.sw_if_index, dst_if.sw_if_index) payload = self.info_to_payload(pkt_info) @@ -107,8 +104,9 @@ class TestSpan(VppTestCase): last_info[i.sw_if_index] = None dst_sw_if_index = dst_if.sw_if_index if len(capture_pg1) != len(capture_pg2): - error("Diffrent number of outgoing and mirrored packets : %u != %u" - % (len(capture_pg1), len(capture_pg2))) + self.logger.error( + "Different number of outgoing and mirrored packets : %u != %u" % + (len(capture_pg1), len(capture_pg2))) raise for pkt_pg1, pkt_pg2 in zip(capture_pg1, capture_pg2): try: @@ -117,23 +115,28 @@ class TestSpan(VppTestCase): raw1 = pkt_pg1[Raw] if pkt_pg1[Ether] != pkt_pg2[Ether]: - error("Diffrent ethernet header of outgoing and mirrored packet") + self.logger.error("Different ethernet header of " + "outgoing and mirrored packet") raise if ip1 != pkt_pg2[IP]: - error("Diffrent ip header of outgoing and mirrored packet") + self.logger.error( + "Different ip header of outgoing and mirrored packet") raise if udp1 != pkt_pg2[UDP]: - error("Diffrent udp header of outgoing and mirrored packet") + self.logger.error( + "Different udp header of outgoing and mirrored packet") raise if raw1 != pkt_pg2[Raw]: - error("Diffrent raw data of outgoing and mirrored packet") + self.logger.error( + "Different raw data of outgoing and mirrored packet") raise payload_info = self.payload_to_info(str(raw1)) packet_index = payload_info.index self.assertEqual(payload_info.dst, dst_sw_if_index) - debug("Got packet on port %s: src=%u (id=%u)" % - (dst_if.name, payload_info.src, packet_index)) + self.logger.debug( + "Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) next_info = self.get_next_packet_info_for_interface2( payload_info.src, dst_sw_if_index, last_info[payload_info.src]) @@ -147,9 +150,9 @@ class TestSpan(VppTestCase): self.assertEqual(udp1.sport, saved_packet[UDP].sport) self.assertEqual(udp1.dport, saved_packet[UDP].dport) except: - error("Unexpected or invalid packet:") - pkt_pg1.show() - pkt_pg2.show() + self.logger.error("Unexpected or invalid packets:") + self.logger.error(ppp("pg1 packet:", pkt_pg1)) + self.logger.error(ppp("pg2 packet:", pkt_pg2)) raise for i in self.interfaces: remaining_packet = self.get_next_packet_info_for_interface2( @@ -164,7 +167,8 @@ class TestSpan(VppTestCase): Test scenario: 1. config 3 interfaces, pg0 l2xconnected with pg1 - 2. sending l2 eth packets between 2 interfaces (pg0, pg1) and mirrored to pg2 + 2. sending l2 eth packets between 2 interfaces (pg0, pg1) and + mirrored to pg2 64B, 512B, 1518B, 9018B (ether_size) burst of packets per interface """ @@ -178,8 +182,11 @@ class TestSpan(VppTestCase): self.pg_start() # Verify packets outgoing packet streams on mirrored interface (pg2) - info("Verifying capture on interfaces %s and %s" % (self.pg1.name, self.pg2.name)) - self.verify_capture(self.pg1, self.pg1.get_capture(), self.pg2.get_capture()) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + self.verify_capture(self.pg1, + self.pg1.get_capture(), + self.pg2.get_capture()) if __name__ == '__main__': diff --git a/test/test_vxlan.py b/test/test_vxlan.py index cb7e7acf..ac435852 100644 --- a/test/test_vxlan.py +++ b/test/test_vxlan.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import unittest -from logging import * from framework import VppTestCase, VppTestRunner from template_bd import BridgeDomain @@ -94,7 +93,7 @@ class TestVxlan(BridgeDomain, VppTestCase): def tearDown(self): super(TestVxlan, self).tearDown() if not self.vpp_dead: - info(self.vapi.cli("show bridge-domain 1 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 1 detail")) if __name__ == '__main__': unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index 6e7e275c..643377f5 100644 --- a/test/util.py +++ b/test/util.py @@ -1,4 +1,18 @@ import socket +import sys +from cStringIO import StringIO + + +def ppp(headline, packet): + """ Return string containing the output of scapy packet.show() call. """ + o = StringIO() + old_stdout = sys.stdout + sys.stdout = o + print(headline) + packet.show() + sys.stdout = old_stdout + return o.getvalue() + class Host(object): """ Generic test host "connected" to VPPs interface. """ diff --git a/test/vpp_interface.py b/test/vpp_interface.py index a450560e..024aeb5f 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -1,8 +1,6 @@ from abc import abstractmethod, ABCMeta import socket -from util import Host - class VppInterface(object): """Generic VPP interface.""" @@ -127,7 +125,8 @@ class VppInterface(object): self._hosts_by_mac = {} self._hosts_by_ip4 = {} self._hosts_by_ip6 = {} - for i in range(2, count+2): # 0: network address, 1: local vpp address + for i in range( + 2, count + 2): # 0: network address, 1: local vpp address mac = "02:%02x:00:00:ff:%02x" % (self.sw_if_index, i) ip4 = "172.16.%u.%u" % (self.sw_if_index, i) ip6 = "fd01:%04x::%04x" % (self.sw_if_index, i) @@ -158,10 +157,9 @@ class VppInterface(object): for intf in r: if intf.sw_if_index == self.sw_if_index: self._name = intf.interface_name.split(b'\0', 1)[0] - self._local_mac = ':'.join( - intf.l2_address.encode('hex')[i:i + 2] - for i in range(0, 12, 2) - ) + self._local_mac =\ + ':'.join(intf.l2_address.encode('hex')[i:i + 2] + for i in range(0, 12, 2)) self._dump = intf break else: diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index dc90289e..51cc20ba 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,6 +1,5 @@ import os import array -from logging import error from hook import Hook do_import = True @@ -32,10 +31,11 @@ class VppPapiProvider(object): """ - def __init__(self, name, shm_prefix): + def __init__(self, name, shm_prefix, test_class): self.hook = Hook("vpp-papi-provider") self.name = name self.shm_prefix = shm_prefix + self.test_class = test_class def register_hook(self, hook): """Replace hook registration with new hook @@ -68,7 +68,7 @@ class VppPapiProvider(object): if hasattr(reply, 'retval') and reply.retval != expected_retval: msg = "API call failed, expected retval == %d, got %s" % ( expected_retval, repr(reply)) - error(msg) + self.test_class.test_instance.logger.error(msg) raise Exception(msg) self.hook.after_api(api_fn.__name__, api_args) return reply @@ -497,7 +497,8 @@ class VppPapiProvider(object): ) ) - def sw_interface_span_enable_disable(self, sw_if_index_from, sw_if_index_to, enable=1): + def sw_interface_span_enable_disable( + self, sw_if_index_from, sw_if_index_to, enable=1): """ :param sw_if_index_from: @@ -683,3 +684,5 @@ class VppPapiProvider(object): next_hop_table_id, stack)) + return self.api(vpp_papi.sw_interface_span_enable_disable, + (sw_if_index_from, sw_if_index_to, enable)) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index 81a0fba9..533c4603 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,12 +1,12 @@ import os import time -from logging import error, info from scapy.utils import wrpcap, rdpcap from vpp_interface import VppInterface from scapy.layers.l2 import Ether, ARP from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, \ ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr +from util import ppp class VppPGInterface(VppInterface): @@ -127,8 +127,8 @@ class VppPGInterface(VppInterface): try: output = rdpcap(self.out_path) except IOError: # TODO - error("File %s does not exist, probably because no" - " packets arrived" % self.out_path) + self.test.logger.error("File %s does not exist, probably because no" + " packets arrived" % self.out_path) return [] return output @@ -154,16 +154,18 @@ class VppPGInterface(VppInterface): """ if pg_interface is None: pg_interface = self - info("Sending ARP request for %s on port %s" % - (self.local_ip4, pg_interface.name)) + self.test.logger.info("Sending ARP request for %s on port %s" % + (self.local_ip4, pg_interface.name)) arp_req = self.create_arp_req() pg_interface.add_stream(arp_req) pg_interface.enable_capture() self.test.pg_start() - info(self.test.vapi.cli("show trace")) + self.test.logger.info(self.test.vapi.cli("show trace")) arp_reply = pg_interface.get_capture() if arp_reply is None or len(arp_reply) == 0: - info("No ARP received on port %s" % pg_interface.name) + self.test.logger.info( + "No ARP received on port %s" % + pg_interface.name) return arp_reply = arp_reply[0] # Make Dot1AD packet content recognizable to scapy @@ -172,14 +174,16 @@ class VppPGInterface(VppInterface): arp_reply = Ether(str(arp_reply)) try: if arp_reply[ARP].op == ARP.is_at: - info("VPP %s MAC address is %s " % - (self.name, arp_reply[ARP].hwsrc)) + self.test.logger.info("VPP %s MAC address is %s " % + (self.name, arp_reply[ARP].hwsrc)) self._local_mac = arp_reply[ARP].hwsrc else: - info("No ARP received on port %s" % pg_interface.name) + self.test.logger.info( + "No ARP received on port %s" % + pg_interface.name) except: - error("Unexpected response to ARP request:") - error(arp_reply.show()) + self.test.logger.error( + ppp("Unexpected response to ARP request:", arp_reply)) raise def resolve_ndp(self, pg_interface=None): @@ -191,16 +195,18 @@ class VppPGInterface(VppInterface): """ if pg_interface is None: pg_interface = self - info("Sending NDP request for %s on port %s" % - (self.local_ip6, pg_interface.name)) + self.test.logger.info("Sending NDP request for %s on port %s" % + (self.local_ip6, pg_interface.name)) ndp_req = self.create_ndp_req() pg_interface.add_stream(ndp_req) pg_interface.enable_capture() self.test.pg_start() - info(self.test.vapi.cli("show trace")) + self.test.logger.info(self.test.vapi.cli("show trace")) ndp_reply = pg_interface.get_capture() if ndp_reply is None or len(ndp_reply) == 0: - info("No NDP received on port %s" % pg_interface.name) + self.test.logger.info( + "No NDP received on port %s" % + pg_interface.name) return ndp_reply = ndp_reply[0] # Make Dot1AD packet content recognizable to scapy @@ -210,10 +216,10 @@ class VppPGInterface(VppInterface): try: ndp_na = ndp_reply[ICMPv6ND_NA] opt = ndp_na[ICMPv6NDOptDstLLAddr] - info("VPP %s MAC address is %s " % - (self.name, opt.lladdr)) + self.test.logger.info("VPP %s MAC address is %s " % + (self.name, opt.lladdr)) self._local_mac = opt.lladdr except: - error("Unexpected response to NDP request:") - error(ndp_reply.show()) + self.test.logger.error( + ppp("Unexpected response to NDP request:", ndp_reply)) raise -- cgit 1.2.3-korg From 0e3c0de1ed87f3cdf16e26e05e39ea6eebeafb18 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 29 Sep 2016 14:43:44 +0200 Subject: BFD: basic asynchronous session up/down This is a work-in-progress basic BFD session handling. Only asynchronous mode is supported at the moment. Setting the session flags doesn't work. Change-Id: Idba27f721b5c35be5a66a6d202a63d23ff7ecf6f Signed-off-by: Klement Sekera --- Makefile | 2 + test/bfd.py | 216 ++++++++ test/framework.py | 28 ++ test/hook.py | 20 +- test/template_bd.py | 3 +- test/test_bfd.py | 273 +++++++++++ test/util.py | 22 + test/vpp_object.py | 79 +++ test/vpp_papi_provider.py | 435 +++++++++------- test/vpp_pg_interface.py | 40 +- vlib-api/vlibapi/api_helper_macros.h | 3 +- vnet/Makefile.am | 16 + vnet/vnet/api_errno.h | 4 +- vnet/vnet/bfd/bfd_api.h | 46 ++ vnet/vnet/bfd/bfd_debug.h | 80 +++ vnet/vnet/bfd/bfd_doc.md | 1 + vnet/vnet/bfd/bfd_main.c | 928 +++++++++++++++++++++++++++++++++++ vnet/vnet/bfd/bfd_main.h | 218 ++++++++ vnet/vnet/bfd/bfd_protocol.c | 74 +++ vnet/vnet/bfd/bfd_protocol.h | 154 ++++++ vnet/vnet/bfd/bfd_udp.c | 610 +++++++++++++++++++++++ vnet/vnet/bfd/bfd_udp.h | 56 +++ vnet/vnet/bfd/dir.dox | 18 + vnet/vnet/buffer.h | 4 + vnet/vnet/ip/ip4_forward.c | 12 +- vnet/vnet/ip/udp.h | 2 + vpp/vpp-api/api.c | 168 ++++++- vpp/vpp-api/vpe.api | 185 +++++++ vppinfra/vppinfra/mhash.c | 4 +- vppinfra/vppinfra/mhash.h | 4 +- 30 files changed, 3504 insertions(+), 201 deletions(-) create mode 100644 test/bfd.py create mode 100644 test/test_bfd.py create mode 100644 test/vpp_object.py create mode 100644 vnet/vnet/bfd/bfd_api.h create mode 100644 vnet/vnet/bfd/bfd_debug.h create mode 100644 vnet/vnet/bfd/bfd_doc.md create mode 100644 vnet/vnet/bfd/bfd_main.c create mode 100644 vnet/vnet/bfd/bfd_main.h create mode 100644 vnet/vnet/bfd/bfd_protocol.c create mode 100644 vnet/vnet/bfd/bfd_protocol.h create mode 100644 vnet/vnet/bfd/bfd_udp.c create mode 100644 vnet/vnet/bfd/bfd_udp.h create mode 100644 vnet/vnet/bfd/dir.dox (limited to 'test/util.py') diff --git a/Makefile b/Makefile index d535389a..00699a76 100644 --- a/Makefile +++ b/Makefile @@ -214,6 +214,8 @@ build-vpp-api: $(BR)/.bootstrap.ok VPP_PYTHON_PREFIX=$(BR)/python + + #$(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-install ,) define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install,) make -C test \ diff --git a/test/bfd.py b/test/bfd.py new file mode 100644 index 00000000..beacd80f --- /dev/null +++ b/test/bfd.py @@ -0,0 +1,216 @@ +from socket import AF_INET, AF_INET6 +from scapy.all import * +from scapy.packet import * +from scapy.fields import * +from framework import * +from vpp_object import * +from util import NumericConstant + + +class BFDDiagCode(NumericConstant): + """ BFD Diagnostic Code """ + no_diagnostic = 0 + control_detection_time_expired = 1 + echo_function_failed = 2 + neighbor_signaled_session_down = 3 + forwarding_plane_reset = 4 + path_down = 5 + concatenated_path_down = 6 + administratively_down = 7 + reverse_concatenated_path_down = 8 + + desc_dict = { + no_diagnostic: "No diagnostic", + control_detection_time_expired: "Control Detection Time Expired", + echo_function_failed: "Echo Function Failed", + neighbor_signaled_session_down: "Neighbor Signaled Session Down", + forwarding_plane_reset: "Forwarding Plane Reset", + path_down: "Path Down", + concatenated_path_down: "Concatenated Path Down", + administratively_down: "Administratively Down", + reverse_concatenated_path_down: "Reverse Concatenated Path Down", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFDState(NumericConstant): + """ BFD State """ + admin_down = 0 + down = 1 + init = 2 + up = 3 + + desc_dict = { + admin_down: "AdminDown", + down: "Down", + init: "Init", + up: "Up", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFD(Packet): + + udp_dport = 3784 #: BFD destination port per RFC 5881 + udp_sport_min = 49152 #: BFD source port min value per RFC 5881 + udp_sport_max = 65535 #: BFD source port max value per RFC 5881 + + name = "BFD" + + fields_desc = [ + BitField("version", 1, 3), + BitEnumField("diag", 0, 5, BFDDiagCode.desc_dict), + BitEnumField("state", 0, 2, BFDState.desc_dict), + FlagsField("flags", 0, 6, ['P', 'F', 'C', 'A', 'D', 'M']), + XByteField("detect_mult", 0), + XByteField("length", 24), + BitField("my_discriminator", 0, 32), + BitField("your_discriminator", 0, 32), + BitField("desired_min_tx_interval", 0, 32), + BitField("required_min_rx_interval", 0, 32), + BitField("required_min_echo_rx_interval", 0, 32)] + + def mysummary(self): + return self.sprintf("BFD(my_disc=%BFD.my_discriminator%," + "your_disc=%BFD.your_discriminator%)") + +# glue the BFD packet class to scapy parser +bind_layers(UDP, BFD, dport=BFD.udp_dport) + + +class VppBFDUDPSession(VppObject): + """ Represents BFD UDP session in VPP """ + + @property + def test(self): + """ Test which created this session """ + return self._test + + @property + def interface(self): + """ Interface on which this session lives """ + return self._interface + + @property + def af(self): + """ Address family - AF_INET or AF_INET6 """ + return self._af + + @property + def bs_index(self): + """ BFD session index from VPP """ + if self._bs_index is not None: + return self._bs_index + raise NotConfiguredException("not configured") + + @property + def local_addr(self): + """ BFD session local address (VPP address) """ + if self._local_addr is None: + return self._interface.local_ip4 + return self._local_addr + + @property + def local_addr_n(self): + """ BFD session local address (VPP address) - raw, suitable for API """ + if self._local_addr is None: + return self._interface.local_ip4n + return self._local_addr_n + + @property + def peer_addr(self): + """ BFD session peer address """ + return self._peer_addr + + @property + def peer_addr_n(self): + """ BFD session peer address - raw, suitable for API """ + return self._peer_addr_n + + @property + def state(self): + """ BFD session state """ + result = self.test.vapi.bfd_udp_session_dump() + session = None + for s in result: + if s.sw_if_index == self.interface.sw_if_index: + if self.af == AF_INET \ + and s.is_ipv6 == 0 \ + and self.interface.local_ip4n == s.local_addr[:4] \ + and self.interface.remote_ip4n == s.peer_addr[:4]: + session = s + break + if session is None: + raise Exception( + "Could not find BFD session in VPP response: %s" % repr(result)) + return session.state + + @property + def desired_min_tx(self): + return self._desired_min_tx + + @property + def required_min_rx(self): + return self._required_min_rx + + @property + def detect_mult(self): + return self._detect_mult + + def __init__(self, test, interface, peer_addr, local_addr=None, af=AF_INET): + self._test = test + self._interface = interface + self._af = af + self._local_addr = local_addr + self._peer_addr = peer_addr + self._peer_addr_n = socket.inet_pton(af, peer_addr) + self._bs_index = None + self._desired_min_tx = 200000 # 0.2s + self._required_min_rx = 200000 # 0.2s + self._detect_mult = 3 # 3 packets need to be missed + + def add_vpp_config(self): + is_ipv6 = 1 if AF_INET6 == self.af else 0 + result = self.test.vapi.bfd_udp_add( + self._interface.sw_if_index, + self.desired_min_tx, + self.required_min_rx, + self.detect_mult, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + self._bs_index = result.bs_index + + def query_vpp_config(self): + result = self.test.vapi.bfd_udp_session_dump() + session = None + for s in result: + if s.sw_if_index == self.interface.sw_if_index: + if self.af == AF_INET \ + and s.is_ipv6 == 0 \ + and self.interface.local_ip4n == s.local_addr[:4] \ + and self.interface.remote_ip4n == s.peer_addr[:4]: + session = s + break + if session is None: + return False + return True + + def remove_vpp_config(self): + if hasattr(self, '_bs_index'): + is_ipv6 = 1 if AF_INET6 == self._af else 0 + self.test.vapi.bfd_udp_del( + self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + + def object_id(self): + return "bfd-udp-%d" % self.bs_index + + def admin_up(self): + self.test.vapi.bfd_session_set_flags(self.bs_index, 1) diff --git a/test/framework.py b/test/framework.py index b3cbb08a..aa4f2fdf 100644 --- a/test/framework.py +++ b/test/framework.py @@ -462,6 +462,34 @@ class VppTestCase(unittest.TestCase): if info.dst == dst_index: return info + def assert_equal(self, real_value, expected_value, name_or_class=None): + if name_or_class is None: + self.assertEqual(real_value, expected_value, msg) + return + try: + msg = "Invalid %s: %d('%s') does not match expected value %d('%s')" + msg = msg % (getdoc(name_or_class).strip(), + real_value, str(name_or_class(real_value)), + expected_value, str(name_or_class(expected_value))) + except: + msg = "Invalid %s: %s does not match expected value %s" % ( + name_or_class, real_value, expected_value) + + self.assertEqual(real_value, expected_value, msg) + + def assert_in_range( + self, + real_value, + expected_min, + expected_max, + name=None): + if name is None: + msg = None + else: + msg = "Invalid %s: %s out of range <%s,%s>" % ( + name, real_value, expected_min, expected_max) + self.assertTrue(expected_min <= real_value <= expected_max, msg) + class VppTestResult(unittest.TestResult): """ diff --git a/test/hook.py b/test/hook.py index 90e9bbf5..f3e5f880 100644 --- a/test/hook.py +++ b/test/hook.py @@ -100,9 +100,13 @@ class PollHook(Hook): signaldict = dict( (k, v) for v, k in reversed(sorted(signal.__dict__.items())) if v.startswith('SIG') and not v.startswith('SIG_')) + + if self.testcase.vpp.returncode in signaldict: + s = signaldict[abs(self.testcase.vpp.returncode)] + else: + s = "unknown" msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( - self.testcase.vpp.returncode, - signaldict[abs(self.testcase.vpp.returncode)]) + self.testcase.vpp.returncode, s) self.logger.critical(msg) core_path = self.testcase.tempdir + '/core' if os.path.isfile(core_path): @@ -110,27 +114,27 @@ class PollHook(Hook): self.testcase.vpp_dead = True raise VppDiedError(msg) - def after_api(self, api_name, api_args): + def before_api(self, api_name, api_args): """ - Check if VPP died after executing an API + Check if VPP died before executing an API :param api_name: name of the API :param api_args: tuple containing the API arguments :raises VppDiedError: exception if VPP is not running anymore """ - super(PollHook, self).after_api(api_name, api_args) + super(PollHook, self).before_api(api_name, api_args) self.poll_vpp() - def after_cli(self, cli): + def before_cli(self, cli): """ - Check if VPP died after executing a CLI + Check if VPP died before executing a CLI :param cli: CLI string :raises Exception: exception if VPP is not running anymore """ - super(PollHook, self).after_cli(cli) + super(PollHook, self).before_cli(cli) self.poll_vpp() diff --git a/test/template_bd.py b/test/template_bd.py index 6c6fb3da..01e8b855 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from abc import abstractmethod +from abc import abstractmethod, ABCMeta from scapy.layers.l2 import Ether, Raw from scapy.layers.inet import IP, UDP @@ -8,6 +8,7 @@ from scapy.layers.inet import IP, UDP class BridgeDomain(object): """ Bridge domain abstraction """ + __metaclass__ = ABCMeta @property def frame_pg0_to_pg1(self): diff --git a/test/test_bfd.py b/test/test_bfd.py new file mode 100644 index 00000000..20d3aea5 --- /dev/null +++ b/test/test_bfd.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python + +import unittest +import time +from random import randint +from bfd import * +from framework import * +from util import ppp + + +class BFDCLITestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) - CLI""" + + @classmethod + def setUpClass(cls): + super(BFDCLITestCase, cls).setUpClass() + + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip4() + cls.pg0.resolve_arp() + + except Exception: + super(BFDCLITestCase, cls).tearDownClass() + raise + + def test_add_bfd(self): + """ create a BFD session """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + self.logger.debug("Session state is %s" % str(session.state)) + session.remove_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + self.logger.debug("Session state is %s" % str(session.state)) + session.remove_vpp_config() + + def test_double_add(self): + """ create the same BFD session twice (negative case) """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + try: + session.add_vpp_config() + except: + session.remove_vpp_config() + return + session.remove_vpp_config() + raise Exception("Expected failure while adding duplicate " + "configuration") + + +def create_packet(interface, ttl=255, src_port=50000, **kwargs): + p = (Ether(src=interface.remote_mac, dst=interface.local_mac) / + IP(src=interface.remote_ip4, dst=interface.local_ip4, ttl=ttl) / + UDP(sport=src_port, dport=BFD.udp_dport) / + BFD(*kwargs)) + return p + + +def verify_ip(test, packet, local_ip, remote_ip): + """ Verify correctness of IP layer. """ + ip = packet[IP] + test.assert_equal(ip.src, local_ip, "IP source address") + test.assert_equal(ip.dst, remote_ip, "IP destination address") + test.assert_equal(ip.ttl, 255, "IP TTL") + + +def verify_udp(test, packet): + """ Verify correctness of UDP layer. """ + udp = packet[UDP] + test.assert_equal(udp.dport, BFD.udp_dport, "UDP destination port") + test.assert_in_range(udp.sport, BFD.udp_sport_min, BFD.udp_sport_max, + "UDP source port") + + +class BFDTestSession(object): + + def __init__(self, test, interface, detect_mult=3): + self.test = test + self.interface = interface + self.bfd_values = { + 'my_discriminator': 0, + 'desired_min_tx_interval': 500000, + 'detect_mult': detect_mult, + 'diag': BFDDiagCode.no_diagnostic, + } + + def update(self, **kwargs): + self.bfd_values.update(kwargs) + + def create_packet(self): + packet = create_packet(self.interface) + for name, value in self.bfd_values.iteritems(): + packet[BFD].setfieldval(name, value) + return packet + + def send_packet(self): + p = self.create_packet() + self.test.logger.debug(ppp("Sending packet:", p)) + self.test.pg0.add_stream([p]) + self.test.pg_start() + + def verify_packet(self, packet): + """ Verify correctness of BFD layer. """ + bfd = packet[BFD] + self.test.assert_equal(bfd.version, 1, "BFD version") + self.test.assert_equal(bfd.your_discriminator, + self.bfd_values['my_discriminator'], + "BFD - your discriminator") + + +class BFDTestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD)""" + + @classmethod + def setUpClass(cls): + super(BFDTestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0, 1]) + cls.pg0.config_ip4() + cls.pg0.generate_remote_hosts() + cls.pg0.configure_ipv4_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFDTestCase, cls).tearDownClass() + raise + + def setUp(self): + self.vapi.want_bfd_events() + self.vpp_session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0) + + def tearDown(self): + self.vapi.want_bfd_events(enable_disable=0) + if not self.vpp_dead: + self.vpp_session.remove_vpp_config() + super(BFDTestCase, self).tearDown() + + def verify_event(self, event, expected_state): + """ Verify correctness of event values. """ + e = event + self.logger.debug("Event: %s" % repr(e)) + self.assert_equal(e.bs_index, self.vpp_session.bs_index, + "BFD session index") + self.assert_equal(e.sw_if_index, self.vpp_session.interface.sw_if_index, + "BFD interface index") + is_ipv6 = 0 + if self.vpp_session.af == AF_INET6: + is_ipv6 = 1 + self.assert_equal(e.is_ipv6, is_ipv6, "is_ipv6") + if self.vpp_session.af == AF_INET: + self.assert_equal(e.local_addr[:4], self.vpp_session.local_addr_n, + "Local IPv4 address") + self.assert_equal(e.peer_addr[:4], self.vpp_session.peer_addr_n, + "Peer IPv4 address") + else: + self.assert_equal(e.local_addr, self.vpp_session.local_addr_n, + "Local IPv6 address") + self.assert_equal(e.peer_addr, self.vpp_session.peer_addr_n, + "Peer IPv6 address") + self.assert_equal(e.state, expected_state, BFDState) + + def wait_for_bfd_packet(self, timeout=1): + p = self.pg0.wait_for_packet(timeout=timeout) + bfd = p[BFD] + if bfd is None: + raise Exception(ppp("Unexpected or invalid BFD packet:", p)) + if bfd.payload: + raise Exception(ppp("Unexpected payload in BFD packet:", bfd)) + verify_ip(self, p, self.pg0.local_ip4, self.pg0.remote_ip4) + verify_udp(self, p) + self.test_session.verify_packet(p) + return p + + def test_slow_timer(self): + """ Slow timer """ + + self.pg_enable_capture([self.pg0]) + expected_packets = 10 + self.logger.info("Waiting for %d BFD packets" % expected_packets) + self.wait_for_bfd_packet() + for i in range(expected_packets): + before = time.time() + self.wait_for_bfd_packet() + after = time.time() + self.assert_in_range( + after - before, 0.75, 1, "time between slow packets") + before = after + + def test_zero_remote_min_rx(self): + """ Zero RemoteMinRxInterval """ + self.pg_enable_capture([self.pg0]) + p = self.wait_for_bfd_packet() + self.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init, + required_min_rx_interval=0) + self.test_session.send_packet() + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.up) + + try: + p = self.pg0.wait_for_packet(timeout=1) + except: + return + raise Exception(ppp("Received unexpected BFD packet:", p)) + + def bfd_conn_up(self): + self.pg_enable_capture([self.pg0]) + self.logger.info("Waiting for slow hello") + p = self.wait_for_bfd_packet() + self.logger.info("Sending Init") + self.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init, + required_min_rx_interval=500000) + self.test_session.send_packet() + self.logger.info("Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.up) + self.logger.info("Session is Up") + self.test_session.update(state=BFDState.up) + + def test_conn_up(self): + """ Basic connection up """ + self.bfd_conn_up() + + def test_hold_up(self): + """ Hold BFD up """ + self.bfd_conn_up() + for i in range(5): + self.wait_for_bfd_packet() + self.test_session.send_packet() + + def test_conn_down(self): + """ Session down after inactivity """ + self.bfd_conn_up() + self.wait_for_bfd_packet() + self.assert_equal( + 0, len(self.vapi.collect_events()), + "number of bfd events") + self.wait_for_bfd_packet() + self.assert_equal( + 0, len(self.vapi.collect_events()), + "number of bfd events") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.down) + + @unittest.skip("this test is not working yet") + def test_large_required_min_rx(self): + self.bfd_conn_up() + interval = 5000000 + self.test_session.update(required_min_rx_interval=interval) + self.test_session.send_packet() + now = time.time() + count = 1 + while time.time() < now + interval / 1000000: + try: + p = self.wait_for_bfd_packet() + if count > 1: + self.logger.error(ppp("Received unexpected packet:", p)) + count += 1 + except: + pass + self.assert_equal(count, 1, "number of packets received") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index 643377f5..f6c6acd4 100644 --- a/test/util.py +++ b/test/util.py @@ -1,5 +1,6 @@ import socket import sys +from abc import abstractmethod, ABCMeta from cStringIO import StringIO @@ -14,6 +15,27 @@ def ppp(headline, packet): return o.getvalue() +class NumericConstant(object): + __metaclass__ = ABCMeta + + desc_dict = {} + + @abstractmethod + def __init__(self, value): + self._value = value + + def __int__(self): + return self._value + + def __long__(self): + return self._value + + def __str__(self): + if self._value in self.desc_dict: + return self.desc_dict[self._value] + return "" + + class Host(object): """ Generic test host "connected" to VPPs interface. """ diff --git a/test/vpp_object.py b/test/vpp_object.py new file mode 100644 index 00000000..2b71fc1f --- /dev/null +++ b/test/vpp_object.py @@ -0,0 +1,79 @@ +from abc import ABCMeta, abstractmethod + + +class VppObject(object): + """ Abstract vpp object """ + __metaclass__ = ABCMeta + + def __init__(self): + VppObjectRegistry().register(self) + + @abstractmethod + def add_vpp_config(self): + """ Add the configuration for this object to vpp. """ + pass + + @abstractmethod + def query_vpp_config(self): + """Query the vpp configuration. + + :return: True if the object is configured""" + pass + + @abstractmethod + def remove_vpp_config(self): + """ Remove the configuration for this object from vpp. """ + pass + + @abstractmethod + def object_id(self): + """ Return a unique string representing this object. """ + pass + + +class VppObjectRegistry(object): + """ Class which handles automatic configuration cleanup. """ + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + if not hasattr(self, "_object_registry"): + self._object_registry = [] + if not hasattr(self, "_object_dict"): + self._object_dict = dict() + + def register(self, o): + """ Register an object in the registry. """ + if not o.unique_id() in self._object_dict: + self._object_registry.append(o) + self._object_dict[o.unique_id()] = o + else: + print "not adding duplicate %s" % o + + def remove_vpp_config(self, logger): + """ + Remove configuration (if present) from vpp and then remove all objects + from the registry. + """ + if not self._object_registry: + logger.info("No objects registered for auto-cleanup.") + return + logger.info("Removing VPP configuration for registered objects") + for o in reversed(self._object_registry): + if o.query_vpp_config(): + logger.info("Removing %s", o) + o.remove_vpp_config() + else: + logger.info("Skipping %s, configuration not present", o) + failed = [] + for o in self._object_registry: + if o.query_vpp_config(): + failed.append(o) + self._object_registry = [] + self._object_dict = dict() + if failed: + logger.error("Couldn't remove configuration for object(s):") + for x in failed: + logger.error(repr(x)) + raise Exception("Couldn't remove configuration for object(s): %s" % + (", ".join(str(x) for x in failed))) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 2f931803..5e80a03e 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,7 +1,12 @@ -import os, fnmatch -import array +import os +import fnmatch +import time from hook import Hook +# Sphinx creates auto-generated documentation by importing the python source +# files and collecting the docstrings from them. The NO_VPP_PAPI flag allows the +# vpp_papi_provider.py file to be importable without having to build the whole +# vpp api if the user only wishes to generate the test documentation. do_import = True try: no_vpp_papi = os.getenv("NO_VPP_PAPI") @@ -17,9 +22,11 @@ if do_import: MPLS_IETF_MAX_LABEL = 0xfffff MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 + class L2_VTR_OP: L2_POP_1 = 3 + class VppPapiProvider(object): """VPP-api provider using vpp-papi @@ -35,12 +42,13 @@ class VppPapiProvider(object): self.test_class = test_class jsonfiles = [] - install_dir=os.getenv('VPP_TEST_INSTALL_PATH') + install_dir = os.getenv('VPP_TEST_INSTALL_PATH') for root, dirnames, filenames in os.walk(install_dir): for filename in fnmatch.filter(filenames, '*.api.json'): jsonfiles.append(os.path.join(root, filename)) self.papi = VPP(jsonfiles) + self._events = list() def register_hook(self, hook): """Replace hook registration with new hook @@ -50,9 +58,35 @@ class VppPapiProvider(object): """ self.hook = hook + def collect_events(self): + e = self._events + self._events = list() + return e + + def wait_for_event(self, timeout, name=None): + limit = time.time() + timeout + while time.time() < limit: + if self._events: + e = self._events.pop(0) + if name and type(e).__name__ != name: + raise Exception( + "Unexpected event received: %s, expected: %s" % + (type(e).__name__, name)) + return e + time.sleep(0) # yield + if name is not None: + raise Exception("Event %s did not occur within timeout" % name) + raise Exception("Event did not occur within timeout") + + def __call__(self, name, event): + # FIXME use the name instead of relying on type(e).__name__ ? + # FIXME #2 if this throws, it is eaten silently, Ole? + self._events.append(event) + def connect(self): """Connect the API to VPP""" self.papi.connect(self.name, self.shm_prefix) + self.papi.register_event_callback(self) def disconnect(self): """Disconnect the API from VPP""" @@ -73,7 +107,7 @@ class VppPapiProvider(object): if hasattr(reply, 'retval') and reply.retval != expected_retval: msg = "API call failed, expected retval == %d, got %s" % ( expected_retval, repr(reply)) - self.test_class.test_instance.logger.error(msg) + self.test_class.logger.error(msg) raise Exception(msg) self.hook.after_api(api_fn.__name__, api_args) return reply @@ -116,7 +150,7 @@ class VppPapiProvider(object): """ return self.api(self.papi.pg_create_interface, - { "interface_id" : pg_index }) + {"interface_id": pg_index}) def sw_interface_dump(self, filter=None): """ @@ -125,7 +159,7 @@ class VppPapiProvider(object): """ if filter is not None: - args = {"name_filter_valid" : 1, "name_filter" : filter} + args = {"name_filter_valid": 1, "name_filter": filter} else: args = {} return self.api(self.papi.sw_interface_dump, args) @@ -140,8 +174,8 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_table, - { 'sw_if_index' : sw_if_index, 'is_ipv6' : is_ipv6, - 'vrf_id' : table_id}) + {'sw_if_index': sw_if_index, 'is_ipv6': is_ipv6, + 'vrf_id': table_id}) def sw_interface_add_del_address(self, sw_if_index, addr, addr_len, is_ipv6=0, is_add=1, del_all=0): @@ -156,12 +190,12 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_add_del_address, - { 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'del_all' : del_all, - 'address_length' : addr_len, - 'address' : addr}) + {'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'del_all': del_all, + 'address_length': addr_len, + 'address': addr}) def sw_interface_enable_disable_mpls(self, sw_if_index, is_enable=1): @@ -172,12 +206,12 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_mpls_enable, - {'sw_if_index' : sw_if_index, - 'enable' : is_enable }) + {'sw_if_index': sw_if_index, + 'enable': is_enable}) def sw_interface_ra_suppress(self, sw_if_index): return self.api(self.papi.sw_interface_ip6nd_ra_config, - {'sw_if_index' : sw_if_index }) + {'sw_if_index': sw_if_index}) def vxlan_add_del_tunnel( self, @@ -202,14 +236,14 @@ class VppPapiProvider(object): """ return self.api(self.papi.vxlan_add_del_tunnel, - {'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'src_address' : src_addr, - 'dst_address' : dst_addr, - 'mcast_sw_if_index' : mcast_sw_if_index, - 'encap_vrf_id' : encap_vrf_id, - 'decap_next_index' : decap_next_index, - 'vni' : vni}) + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'src_address': src_addr, + 'dst_address': dst_addr, + 'mcast_sw_if_index': mcast_sw_if_index, + 'encap_vrf_id': encap_vrf_id, + 'decap_next_index': decap_next_index, + 'vni': vni}) def bridge_domain_add_del(self, bd_id, flood=1, uu_flood=1, forward=1, learn=1, arp_term=0, is_add=1): @@ -229,13 +263,13 @@ class VppPapiProvider(object): :param int is_add: Add or delete flag. (Default value = 1) """ return self.api(self.papi.bridge_domain_add_del, - { 'bd_id' : bd_id, - 'flood' : flood, - 'uu_flood' : uu_flood, - 'forward' : forward, - 'learn' : learn, - 'arp_term' : arp_term, - 'is_add' : is_add}) + {'bd_id': bd_id, + 'flood': flood, + 'uu_flood': uu_flood, + 'forward': forward, + 'learn': learn, + 'arp_term': arp_term, + 'is_add': is_add}) def l2fib_add_del(self, mac, bd_id, sw_if_index, is_add=1, static_mac=0, filter_mac=0, bvi_mac=0): @@ -254,13 +288,13 @@ class VppPapiProvider(object): interface. (Default value = 0) """ return self.api(self.papi.l2fib_add_del, - { 'mac' : self._convert_mac(mac), - 'bd_id' : bd_id, - 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'static_mac' : static_mac, - 'filter_mac' : filter_mac, - 'bvi_mac' : bvi_mac }) + {'mac': self._convert_mac(mac), + 'bd_id': bd_id, + 'sw_if_index': sw_if_index, + 'is_add': is_add, + 'static_mac': static_mac, + 'filter_mac': filter_mac, + 'bvi_mac': bvi_mac}) def sw_interface_set_l2_bridge(self, sw_if_index, bd_id, shg=0, bvi=0, enable=1): @@ -274,11 +308,11 @@ class VppPapiProvider(object): :param int enable: Add or remove interface. (Default value = 1) """ return self.api(self.papi.sw_interface_set_l2_bridge, - { 'rx_sw_if_index' : sw_if_index, - 'bd_id' : bd_id, - 'shg' : shg, - 'bvi' : bvi, - 'enable' : enable }) + {'rx_sw_if_index': sw_if_index, + 'bd_id': bd_id, + 'shg': shg, + 'bvi': bvi, + 'enable': enable}) def bridge_flags(self, bd_id, is_set, feature_bitmap): """Enable/disable required feature of the bridge domain with defined ID. @@ -293,9 +327,9 @@ class VppPapiProvider(object): - arp-term (1 << 4). """ return self.api(self.papi.bridge_flags, - {'bd_id' : bd_id, - 'is_set' : is_set, - 'feature_bitmap' : feature_bitmap }) + {'bd_id': bd_id, + 'is_set': is_set, + 'feature_bitmap': feature_bitmap}) def bridge_domain_dump(self, bd_id=0): """ @@ -305,7 +339,7 @@ class VppPapiProvider(object): :return: Dictionary of bridge domain(s) data. """ return self.api(self.papi.bridge_domain_dump, - {'bd_id' : bd_id }) + {'bd_id': bd_id}) def sw_interface_set_l2_xconnect(self, rx_sw_if_index, tx_sw_if_index, enable): @@ -319,11 +353,17 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_l2_xconnect, - { 'rx_sw_if_index' : rx_sw_if_index, - 'tx_sw_if_index' : tx_sw_if_index, - 'enable' : enable }) + {'rx_sw_if_index': rx_sw_if_index, + 'tx_sw_if_index': tx_sw_if_index, + 'enable': enable}) - def sw_interface_set_l2_tag_rewrite(self, sw_if_index, vtr_oper, push=0, tag1=0, tag2=0): + def sw_interface_set_l2_tag_rewrite( + self, + sw_if_index, + vtr_oper, + push=0, + tag1=0, + tag2=0): """L2 interface vlan tag rewrite configure request :param client_index - opaque cookie to identify the sender :param context - sender context, to match reply w/ request @@ -335,11 +375,11 @@ class VppPapiProvider(object): """ return self.api(self.papi.l2_interface_vlan_tag_rewrite, - { 'sw_if_index' : sw_if_index, - 'vtr_op' : vtr_oper, - 'push_dot1q' : push, - 'tag1' : tag1, - 'tag2' : tag2 }) + {'sw_if_index': sw_if_index, + 'vtr_op': vtr_oper, + 'push_dot1q': push, + 'tag1': tag1, + 'tag2': tag2}) def sw_interface_set_flags(self, sw_if_index, admin_up_down, link_up_down=0, deleted=0): @@ -352,10 +392,10 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_flags, - { 'sw_if_index' : sw_if_index, - 'admin_up_down' : admin_up_down, - 'link_up_down' : link_up_down, - 'deleted' : deleted }) + {'sw_if_index': sw_if_index, + 'admin_up_down': admin_up_down, + 'link_up_down': link_up_down, + 'deleted': deleted}) def create_subif(self, sw_if_index, sub_id, outer_vlan, inner_vlan, no_tags=0, one_tag=0, two_tags=0, dot1ad=0, exact_match=0, @@ -379,18 +419,18 @@ class VppPapiProvider(object): """ return self.api( self.papi.create_subif, - { 'sw_if_index' : sw_if_index, - 'sub_id' : sub_id, - 'no_tags' : no_tags, - 'one_tag' : one_tag, - 'two_tags' : two_tags, - 'dot1ad' : dot1ad, - 'exact_match' : exact_match, - 'default_sub' : default_sub, - 'outer_vlan_id_any' : outer_vlan_id_any, - 'inner_vlan_id_any' : inner_vlan_id_any, - 'outer_vlan_id' : outer_vlan, - 'inner_vlan_id' : inner_vlan }) + {'sw_if_index': sw_if_index, + 'sub_id': sub_id, + 'no_tags': no_tags, + 'one_tag': one_tag, + 'two_tags': two_tags, + 'dot1ad': dot1ad, + 'exact_match': exact_match, + 'default_sub': default_sub, + 'outer_vlan_id_any': outer_vlan_id_any, + 'inner_vlan_id_any': inner_vlan_id_any, + 'outer_vlan_id': outer_vlan, + 'inner_vlan_id': inner_vlan}) def delete_subif(self, sw_if_index): """Delete subinterface @@ -398,7 +438,7 @@ class VppPapiProvider(object): :param sw_if_index: """ return self.api(self.papi.delete_subif, - { 'sw_if_index' : sw_if_index }) + {'sw_if_index': sw_if_index}) def create_vlan_subif(self, sw_if_index, vlan): """ @@ -408,8 +448,8 @@ class VppPapiProvider(object): """ return self.api(self.papi.create_vlan_subif, - {'sw_if_index' : sw_if_index, - 'vlan_id' : vlan }) + {'sw_if_index': sw_if_index, + 'vlan_id': vlan}) def create_loopback(self, mac=''): """ @@ -417,7 +457,7 @@ class VppPapiProvider(object): :param mac: (Optional) """ return self.api(self.papi.create_loopback, - { 'mac_address' : mac }) + {'mac_address': mac}) def ip_add_del_route( self, @@ -428,9 +468,9 @@ class VppPapiProvider(object): table_id=0, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_resolve_host=0, is_resolve_attached=0, @@ -470,29 +510,29 @@ class VppPapiProvider(object): return self.api( self.papi.ip_add_del_route, - { 'next_hop_sw_if_index' : next_hop_sw_if_index, - 'table_id' : table_id, - 'classify_table_index' : classify_table_index, - 'next_hop_table_id' : next_hop_table_id, - 'create_vrf_if_needed' : create_vrf_if_needed, - 'is_add' : is_add, - 'is_drop' : is_drop, - 'is_unreach' : is_unreach, - 'is_prohibit' : is_prohibit, - 'is_ipv6' : is_ipv6, - 'is_local' : is_local, - 'is_classify' : is_classify, - 'is_multipath' : is_multipath, - 'is_resolve_host' : is_resolve_host, - 'is_resolve_attached' : is_resolve_attached, - 'not_last' : not_last, - 'next_hop_weight' : next_hop_weight, - 'dst_address_length' : dst_address_length, - 'dst_address' : dst_address, - 'next_hop_address' : next_hop_address, - 'next_hop_n_out_labels' : next_hop_n_out_labels, - 'next_hop_via_label' : next_hop_via_label, - 'next_hop_out_label_stack' : next_hop_out_label_stack }) + {'next_hop_sw_if_index': next_hop_sw_if_index, + 'table_id': table_id, + 'classify_table_index': classify_table_index, + 'next_hop_table_id': next_hop_table_id, + 'create_vrf_if_needed': create_vrf_if_needed, + 'is_add': is_add, + 'is_drop': is_drop, + 'is_unreach': is_unreach, + 'is_prohibit': is_prohibit, + 'is_ipv6': is_ipv6, + 'is_local': is_local, + 'is_classify': is_classify, + 'is_multipath': is_multipath, + 'is_resolve_host': is_resolve_host, + 'is_resolve_attached': is_resolve_attached, + 'not_last': not_last, + 'next_hop_weight': next_hop_weight, + 'dst_address_length': dst_address_length, + 'dst_address': dst_address, + 'next_hop_address': next_hop_address, + 'next_hop_n_out_labels': next_hop_n_out_labels, + 'next_hop_via_label': next_hop_via_label, + 'next_hop_out_label_stack': next_hop_out_label_stack}) def ip_neighbor_add_del(self, sw_if_index, @@ -516,13 +556,13 @@ class VppPapiProvider(object): return self.api( self.papi.ip_neighbor_add_del, - { 'vrf_id' : vrf_id, - 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'is_static' : is_static, - 'mac_address' : mac_address, - 'dst_address' : dst_address + {'vrf_id': vrf_id, + 'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'is_static': is_static, + 'mac_address': mac_address, + 'dst_address': dst_address } ) @@ -536,9 +576,9 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_span_enable_disable, - { 'sw_if_index_from' : sw_if_index_from, - 'sw_if_index_to' : sw_if_index_to, - 'state' : state }) + {'sw_if_index_from': sw_if_index_from, + 'sw_if_index_to': sw_if_index_to, + 'state': state}) def gre_tunnel_add_del(self, src_address, @@ -559,12 +599,12 @@ class VppPapiProvider(object): return self.api( self.papi.gre_add_del_tunnel, - { 'is_add' : is_add, - 'is_ipv6' : is_ip6, - 'teb' : is_teb, - 'src_address' : src_address, - 'dst_address' : dst_address, - 'outer_fib_id' : outer_fib_id } + {'is_add': is_add, + 'is_ipv6': is_ip6, + 'teb': is_teb, + 'src_address': src_address, + 'dst_address': dst_address, + 'outer_fib_id': outer_fib_id} ) def mpls_route_add_del( @@ -577,9 +617,9 @@ class VppPapiProvider(object): table_id=0, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_resolve_host=0, is_resolve_attached=0, @@ -615,24 +655,24 @@ class VppPapiProvider(object): return self.api( self.papi.mpls_route_add_del, - { 'mr_label' : label, - 'mr_eos' : eos, - 'mr_table_id' : table_id, - 'mr_classify_table_index' : classify_table_index, - 'mr_create_table_if_needed' : create_vrf_if_needed, - 'mr_is_add' : is_add, - 'mr_is_classify' : is_classify, - 'mr_is_multipath' : is_multipath, - 'mr_is_resolve_host' : is_resolve_host, - 'mr_is_resolve_attached' : is_resolve_attached, - 'mr_next_hop_proto_is_ip4' : next_hop_proto_is_ip4, - 'mr_next_hop_weight' : next_hop_weight, - 'mr_next_hop' : next_hop_address, - 'mr_next_hop_n_out_labels' : next_hop_n_out_labels, - 'mr_next_hop_sw_if_index' : next_hop_sw_if_index, - 'mr_next_hop_table_id' : next_hop_table_id, - 'mr_next_hop_via_label' : next_hop_via_label, - 'mr_next_hop_out_label_stack' : next_hop_out_label_stack }) + {'mr_label': label, + 'mr_eos': eos, + 'mr_table_id': table_id, + 'mr_classify_table_index': classify_table_index, + 'mr_create_table_if_needed': create_vrf_if_needed, + 'mr_is_add': is_add, + 'mr_is_classify': is_classify, + 'mr_is_multipath': is_multipath, + 'mr_is_resolve_host': is_resolve_host, + 'mr_is_resolve_attached': is_resolve_attached, + 'mr_next_hop_proto_is_ip4': next_hop_proto_is_ip4, + 'mr_next_hop_weight': next_hop_weight, + 'mr_next_hop': next_hop_address, + 'mr_next_hop_n_out_labels': next_hop_n_out_labels, + 'mr_next_hop_sw_if_index': next_hop_sw_if_index, + 'mr_next_hop_table_id': next_hop_table_id, + 'mr_next_hop_via_label': next_hop_via_label, + 'mr_next_hop_out_label_stack': next_hop_out_label_stack}) def mpls_ip_bind_unbind( self, @@ -648,14 +688,14 @@ class VppPapiProvider(object): """ return self.api( self.papi.mpls_ip_bind_unbind, - {'mb_mpls_table_id' : table_id, - 'mb_label' : label, - 'mb_ip_table_id' : ip_table_id, - 'mb_create_table_if_needed' : create_vrf_if_needed, - 'mb_is_bind' : is_bind, - 'mb_is_ip4' : is_ip4, - 'mb_address_length' : dst_address_length, - 'mb_address' : dst_address}) + {'mb_mpls_table_id': table_id, + 'mb_label': label, + 'mb_ip_table_id': ip_table_id, + 'mb_create_table_if_needed': create_vrf_if_needed, + 'mb_is_bind': is_bind, + 'mb_is_ip4': is_ip4, + 'mb_address_length': dst_address_length, + 'mb_address': dst_address}) def mpls_tunnel_add_del( self, @@ -665,9 +705,9 @@ class VppPapiProvider(object): next_hop_sw_if_index=0xFFFFFFFF, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_add=1, l2_only=0): @@ -696,19 +736,16 @@ class VppPapiProvider(object): """ return self.api( self.papi.mpls_tunnel_add_del, - {'mt_sw_if_index' : tun_sw_if_index, - 'mt_is_add' : is_add, - 'mt_l2_only' : l2_only, - 'mt_next_hop_proto_is_ip4' : next_hop_proto_is_ip4, - 'mt_next_hop_weight' : next_hop_weight, - 'mt_next_hop' : next_hop_address, - 'mt_next_hop_n_out_labels' : next_hop_n_out_labels, - 'mt_next_hop_sw_if_index' :next_hop_sw_if_index, - 'mt_next_hop_table_id' : next_hop_table_id, - 'mt_next_hop_out_label_stack' : next_hop_out_label_stack }) - - return self.api(vpp_papi.sw_interface_span_enable_disable, - (sw_if_index_from, sw_if_index_to, enable)) + {'mt_sw_if_index': tun_sw_if_index, + 'mt_is_add': is_add, + 'mt_l2_only': l2_only, + 'mt_next_hop_proto_is_ip4': next_hop_proto_is_ip4, + 'mt_next_hop_weight': next_hop_weight, + 'mt_next_hop': next_hop_address, + 'mt_next_hop_n_out_labels': next_hop_n_out_labels, + 'mt_next_hop_sw_if_index': next_hop_sw_if_index, + 'mt_next_hop_table_id': next_hop_table_id, + 'mt_next_hop_out_label_stack': next_hop_out_label_stack}) def snat_interface_add_del_feature( self, @@ -723,9 +760,9 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_interface_add_del_feature, - {'is_add' : is_add, - 'is_inside' : is_inside, - 'sw_if_index' : sw_if_index}) + {'is_add': is_add, + 'is_inside': is_inside, + 'sw_if_index': sw_if_index}) def snat_add_static_mapping( self, @@ -750,14 +787,14 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_add_static_mapping, - {'is_add' : is_add, - 'is_ip4' : is_ip4, - 'addr_only' : addr_only, - 'local_ip_address' : local_ip, - 'external_ip_address' : external_ip, - 'local_port' : local_port, - 'external_port' : external_port, - 'vrf_id' : vrf_id}) + {'is_add': is_add, + 'is_ip4': is_ip4, + 'addr_only': addr_only, + 'local_ip_address': local_ip, + 'external_ip_address': external_ip, + 'local_port': local_port, + 'external_port': external_port, + 'vrf_id': vrf_id}) def snat_add_address_range( self, @@ -774,10 +811,10 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_add_address_range, - {'is_ip4' : is_ip4, - 'first_ip_address' : first_ip_address, - 'last_ip_address' : last_ip_address, - 'is_add' : is_add}) + {'is_ip4': is_ip4, + 'first_ip_address': first_ip_address, + 'last_ip_address': last_ip_address, + 'is_add': is_add}) def snat_address_dump(self): """Dump S-NAT addresses @@ -796,3 +833,43 @@ class VppPapiProvider(object): :return: Dictionary of S-NAT static mappings """ return self.api(self.papi.snat_static_mapping_dump, {}) + + def control_ping(self): + self.api(self.papi.control_ping) + + def bfd_udp_add(self, sw_if_index, desired_min_tx, required_min_rx, + detect_mult, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_add, + { + 'sw_if_index': sw_if_index, + 'desired_min_tx': desired_min_tx, + 'required_min_rx': required_min_rx, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'detect_mult': detect_mult, + }) + + def bfd_udp_del(self, sw_if_index, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_del, + { + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + }) + + def bfd_udp_session_dump(self): + return self.api(self.papi.bfd_udp_session_dump, {}) + + def bfd_session_set_flags(self, bs_idx, admin_up_down): + return self.api(self.papi.bfd_session_set_flags, { + 'bs_index': bs_idx, + 'admin_up_down': admin_up_down, + }) + + def want_bfd_events(self, enable_disable=1): + return self.api(self.papi.want_bfd_events, { + 'enable_disable': enable_disable, + 'pid': os.getpid(), + }) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index 533c4603..012f5768 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,10 +1,10 @@ import os import time -from scapy.utils import wrpcap, rdpcap +from scapy.utils import wrpcap, rdpcap, PcapReader from vpp_interface import VppInterface from scapy.layers.l2 import Ether, ARP -from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, \ +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\ ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr from util import ppp @@ -93,6 +93,7 @@ class VppPGInterface(VppInterface): pass # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.capture_cli) + self._pcap_reader = None def add_stream(self, pkts): """ @@ -132,6 +133,41 @@ class VppPGInterface(VppInterface): return [] return output + def wait_for_packet(self, timeout): + """ + Wait for next packet captured with a timeout + + :param timeout: How long to wait for the packet + + :returns: Captured packet if no packet arrived within timeout + :raises Exception: if no packet arrives within timeout + """ + limit = time.time() + timeout + if self._pcap_reader is None: + self.test.logger.debug("Waiting for the capture file to appear") + while time.time() < limit: + if os.path.isfile(self.out_path): + break + time.sleep(0) # yield + if os.path.isfile(self.out_path): + self.test.logger.debug("Capture file appeared after %fs" % + (time.time() - (limit - timeout))) + self._pcap_reader = PcapReader(self.out_path) + else: + self.test.logger.debug("Timeout - capture file still nowhere") + raise Exception("Packet didn't arrive within timeout") + + self.test.logger.debug("Waiting for packet") + while time.time() < limit: + p = self._pcap_reader.recv() + if p is not None: + self.test.logger.debug("Packet received after %fs", + (time.time() - (limit - timeout))) + return p + time.sleep(0) # yield + self.test.logger.debug("Timeout - no packets received") + raise Exception("Packet didn't arrive within timeout") + def create_arp_req(self): """Create ARP request applicable for this interface""" return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / diff --git a/vlib-api/vlibapi/api_helper_macros.h b/vlib-api/vlibapi/api_helper_macros.h index a1475656..16f34cfc 100644 --- a/vlib-api/vlibapi/api_helper_macros.h +++ b/vlib-api/vlibapi/api_helper_macros.h @@ -197,7 +197,8 @@ _(to_netconf_server) \ _(from_netconf_server) \ _(to_netconf_client) \ _(from_netconf_client) \ -_(oam_events) +_(oam_events) \ +_(bfd_events) /* WARNING: replicated in vpp/stats.h */ typedef struct diff --git a/vnet/Makefile.am b/vnet/Makefile.am index eb7f8ef1..4e4b2c0a 100644 --- a/vnet/Makefile.am +++ b/vnet/Makefile.am @@ -349,6 +349,22 @@ nobase_include_HEADERS += \ vnet/ip/udp.h \ vnet/ip/udp_packet.h +######################################## +# Bidirectional Forwarding Detection +######################################## + +nobase_include_HEADERS += \ + vnet/bfd/bfd_protocol.h \ + vnet/bfd/bfd_main.h \ + vnet/bfd/bfd_api.h \ + vnet/bfd/bfd_udp.h + +libvnet_la_SOURCES += \ + vnet/bfd/bfd_api.h \ + vnet/bfd/bfd_udp.c \ + vnet/bfd/bfd_main.c \ + vnet/bfd/bfd_protocol.c + ######################################## # Layer 3 protocol: IPSec ######################################## diff --git a/vnet/vnet/api_errno.h b/vnet/vnet/api_errno.h index 50d6f731..65e3e591 100644 --- a/vnet/vnet/api_errno.h +++ b/vnet/vnet/api_errno.h @@ -90,7 +90,9 @@ _(EXCEEDED_NUMBER_OF_PORTS_CAPACITY, -96, "Operation would exceed capacity of nu _(INVALID_ADDRESS_FAMILY, -97, "Invalid address family") \ _(INVALID_SUB_SW_IF_INDEX, -98, "Invalid sub-interface sw_if_index") \ _(TABLE_TOO_BIG, -99, "Table too big") \ -_(CANNOT_ENABLE_DISABLE_FEATURE, -100, "Cannot enable/disable feature") +_(CANNOT_ENABLE_DISABLE_FEATURE, -100, "Cannot enable/disable feature") \ +_(BFD_EEXIST, -101, "Duplicate BFD session") \ +_(BFD_NOENT, -102, "No such BFD session") typedef enum { diff --git a/vnet/vnet/bfd/bfd_api.h b/vnet/vnet/bfd/bfd_api.h new file mode 100644 index 00000000..cfcd04f3 --- /dev/null +++ b/vnet/vnet/bfd/bfd_api.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011-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. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_api_h__ +#define __included_bfd_api_h__ + +#include +#include +#include +#include + +vnet_api_error_t bfd_udp_add_session (u32 sw_if_index, u32 desired_min_tx_us, + u32 required_min_rx_us, u8 detect_mult, + const ip46_address_t * local_addr, + const ip46_address_t * peer_addr); + +vnet_api_error_t bfd_udp_del_session (u32 sw_if_index, + const ip46_address_t * local_addr, + const ip46_address_t * peer_addr); + +vnet_api_error_t bfd_session_set_flags (u32 bs_index, u8 admin_up_down); + +#endif /* __included_bfd_api_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_debug.h b/vnet/vnet/bfd/bfd_debug.h new file mode 100644 index 00000000..c11e6d9f --- /dev/null +++ b/vnet/vnet/bfd/bfd_debug.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2011-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. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_debug_h__ +#define __included_bfd_debug_h__ + +/* controls debug prints */ +#define BFD_DEBUG (0) + +#if BFD_DEBUG +#define BFD_DEBUG_FILE_DEF \ + static const char *__file = NULL; \ + if (!__file) \ + { \ + __file = strrchr (__FILE__, '/'); \ + if (__file) \ + { \ + ++__file; \ + } \ + else \ + { \ + __file = __FILE__; \ + } \ + } + +#define BFD_DBG(fmt, ...) \ + do \ + { \ + BFD_DEBUG_FILE_DEF \ + u8 *_s = NULL; \ + vlib_main_t *vm = vlib_get_main (); \ + _s = format (_s, "%6.02f:DBG:%s:%d:%s():" fmt, vlib_time_now (vm), \ + __file, __LINE__, __func__, ##__VA_ARGS__); \ + printf ("%s\n", _s); \ + vec_free (_s); \ + } \ + while (0); + +#define BFD_ERR(fmt, ...) \ + do \ + { \ + BFD_DEBUG_FILE_DEF \ + u8 *_s = NULL; \ + vlib_main_t *vm = vlib_get_main (); \ + _s = format (_s, "%6.02f:ERR:%s:%d:%s():" fmt, vlib_time_now (vm), \ + __file, __LINE__, __func__, ##__VA_ARGS__); \ + printf ("%s\n", _s); \ + vec_free (_s); \ + } \ + while (0); + +#else +#define BFD_DBG(...) +#define BFD_ERR(...) +#endif + +#endif /* __included_bfd_debug_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_doc.md b/vnet/vnet/bfd/bfd_doc.md new file mode 100644 index 00000000..1333ed77 --- /dev/null +++ b/vnet/vnet/bfd/bfd_doc.md @@ -0,0 +1 @@ +TODO diff --git a/vnet/vnet/bfd/bfd_main.c b/vnet/vnet/bfd/bfd_main.c new file mode 100644 index 00000000..a72d6fed --- /dev/null +++ b/vnet/vnet/bfd/bfd_main.c @@ -0,0 +1,928 @@ +/* + * Copyright (c) 2011-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. + */ +/** + * @file + * @brief BFD nodes implementation + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static u64 +bfd_us_to_clocks (bfd_main_t * bm, u64 us) +{ + return bm->cpu_cps * ((f64) us / USEC_PER_SECOND); +} + +static vlib_node_registration_t bfd_process_node; + +typedef enum +{ +#define F(t, n) BFD_OUTPUT_##t, + foreach_bfd_transport (F) +#undef F + BFD_OUTPUT_N_NEXT, +} bfd_output_next_t; + +static u32 bfd_next_index_by_transport[] = { +#define F(t, n) [BFD_TRANSPORT_##t] = BFD_OUTPUT_##t, + foreach_bfd_transport (F) +#undef F +}; + +/* + * We actually send all bfd pkts to the "error" node after scanning + * them, so the graph node has only one next-index. The "error-drop" + * node automatically bumps our per-node packet counters for us. + */ +typedef enum +{ + BFD_INPUT_NEXT_NORMAL, + BFD_INPUT_N_NEXT, +} bfd_input_next_t; + +static void bfd_on_state_change (bfd_main_t * bm, bfd_session_t * bs, + u64 now); + +static void +bfd_set_defaults (bfd_main_t * bm, bfd_session_t * bs) +{ + bs->local_state = BFD_STATE_down; + bs->local_diag = BFD_DIAG_CODE_no_diag; + bs->remote_state = BFD_STATE_down; + bs->local_demand = 0; + bs->remote_discr = 0; + bs->desired_min_tx_us = BFD_DEFAULT_DESIRED_MIN_TX_US; + bs->desired_min_tx_clocks = bfd_us_to_clocks (bm, bs->desired_min_tx_us); + bs->remote_min_rx_us = 1; + bs->remote_demand = 0; +} + +static void +bfd_set_diag (bfd_session_t * bs, bfd_diag_code_e code) +{ + if (bs->local_diag != code) + { + BFD_DBG ("set local_diag, bs_idx=%d: '%d:%s'", bs->bs_idx, code, + bfd_diag_code_string (code)); + bs->local_diag = code; + } +} + +static void +bfd_set_state (bfd_main_t * bm, bfd_session_t * bs, bfd_state_e new_state) +{ + if (bs->local_state != new_state) + { + BFD_DBG ("Change state, bs_idx=%d: %s->%s", bs->bs_idx, + bfd_state_string (bs->local_state), + bfd_state_string (new_state)); + bs->local_state = new_state; + bfd_on_state_change (bm, bs, clib_cpu_time_now ()); + } +} + +static void +bfd_recalc_tx_interval (bfd_main_t * bm, bfd_session_t * bs) +{ + if (!bs->local_demand) + { + bs->transmit_interval_clocks = + clib_max (bs->desired_min_tx_clocks, bs->remote_min_rx_clocks); + } + else + { + /* TODO */ + } + BFD_DBG ("Recalculated transmit interval %lu clocks/%.2fs", + bs->transmit_interval_clocks, + bs->transmit_interval_clocks / bm->cpu_cps); +} + +static void +bfd_calc_next_tx (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (!bs->local_demand) + { + if (bs->local_detect_mult > 1) + { + /* common case - 75-100% of transmit interval */ + bs->tx_timeout_clocks = now + + (1 - .25 * (random_f64 (&bm->random_seed))) * + bs->transmit_interval_clocks; + } + else + { + /* special case - 75-90% of transmit interval */ + bs->tx_timeout_clocks = + now + + (.9 - .15 * (random_f64 (&bm->random_seed))) * + bs->transmit_interval_clocks; + } + } + else + { + /* TODO */ + } + if (bs->tx_timeout_clocks) + { + BFD_DBG ("Next transmit in %lu clocks/%.02fs@%lu", + bs->tx_timeout_clocks - now, + (bs->tx_timeout_clocks - now) / bm->cpu_cps, + bs->tx_timeout_clocks); + } +} + +static void +bfd_recalc_detection_time (bfd_main_t * bm, bfd_session_t * bs) +{ + if (!bs->local_demand) + { + bs->detection_time_clocks = + bs->remote_detect_mult * + bfd_us_to_clocks (bm, clib_max (bs->required_min_rx_us, + bs->remote_desired_min_tx_us)); + } + else + { + bs->detection_time_clocks = + bs->local_detect_mult * + bfd_us_to_clocks (bm, + clib_max (bs->desired_min_tx_us, + bs->remote_min_rx_us)); + } + BFD_DBG ("Recalculated detection time %lu clocks/%.2fs", + bs->detection_time_clocks, + bs->detection_time_clocks / bm->cpu_cps); +} + +static void +bfd_set_timer (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + u64 next = 0; + u64 rx_timeout = 0; + if (BFD_STATE_up == bs->local_state) + { + rx_timeout = bs->last_rx_clocks + bs->detection_time_clocks; + } + if (bs->tx_timeout_clocks && rx_timeout) + { + next = clib_min (bs->tx_timeout_clocks, rx_timeout); + } + else if (bs->tx_timeout_clocks) + { + next = bs->tx_timeout_clocks; + } + else if (rx_timeout) + { + next = rx_timeout; + } + BFD_DBG ("bs_idx=%u, tx_timeout=%lu, rx_timeout=%lu, next=%s", bs->bs_idx, + bs->tx_timeout_clocks, rx_timeout, + next == bs->tx_timeout_clocks ? "tx" : "rx"); + if (next && (next < bs->wheel_time_clocks || !bs->wheel_time_clocks)) + { + if (bs->wheel_time_clocks) + { + timing_wheel_delete (&bm->wheel, bs->bs_idx); + } + bs->wheel_time_clocks = next; + BFD_DBG ("timing_wheel_insert(%p, %lu (%ld clocks/%.2fs in the " + "future), %u);", + &bm->wheel, bs->wheel_time_clocks, + (i64) bs->wheel_time_clocks - clib_cpu_time_now (), + (i64) (bs->wheel_time_clocks - clib_cpu_time_now ()) / + bm->cpu_cps, bs->bs_idx); + timing_wheel_insert (&bm->wheel, bs->wheel_time_clocks, bs->bs_idx); + } +} + +static void +bfd_set_desired_min_tx (bfd_main_t * bm, bfd_session_t * bs, u64 now, + u32 desired_min_tx_us) +{ + bs->desired_min_tx_us = desired_min_tx_us; + bs->desired_min_tx_clocks = bfd_us_to_clocks (bm, bs->desired_min_tx_us); + BFD_DBG ("Set desired min tx to %uus/%lu clocks/%.2fs", + bs->desired_min_tx_us, bs->desired_min_tx_clocks, + bs->desired_min_tx_clocks / bm->cpu_cps); + bfd_recalc_detection_time (bm, bs); + bfd_recalc_tx_interval (bm, bs); + bfd_calc_next_tx (bm, bs, now); + bfd_set_timer (bm, bs, now); +} + +void +bfd_session_start (bfd_main_t * bm, bfd_session_t * bs) +{ + BFD_DBG ("%U", format_bfd_session, bs); + bfd_recalc_tx_interval (bm, bs); + vlib_process_signal_event (bm->vlib_main, bm->bfd_process_node_index, + BFD_EVENT_NEW_SESSION, bs->bs_idx); +} + +vnet_api_error_t +bfd_del_session (uword bs_idx) +{ + const bfd_main_t *bm = &bfd_main; + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + pool_put (bm->sessions, bs); + return 0; + } + else + { + BFD_ERR ("no such session"); + return VNET_API_ERROR_BFD_NOENT; + } + return 0; +} + +const char * +bfd_diag_code_string (bfd_diag_code_e diag) +{ +#define F(n, t, s) \ + case BFD_DIAG_CODE_NAME (t): \ + return s; + switch (diag) + { + foreach_bfd_diag_code (F)} + return "UNKNOWN"; +#undef F +} + +const char * +bfd_state_string (bfd_state_e state) +{ +#define F(n, t, s) \ + case BFD_STATE_NAME (t): \ + return s; + switch (state) + { + foreach_bfd_state (F)} + return "UNKNOWN"; +#undef F +} + +vnet_api_error_t +bfd_session_set_flags (u32 bs_idx, u8 admin_up_down) +{ + bfd_main_t *bm = &bfd_main; + if (pool_is_free_index (bm->sessions, bs_idx)) + { + BFD_ERR ("invalid bs_idx=%u", bs_idx); + return VNET_API_ERROR_BFD_NOENT; + } + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + if (admin_up_down) + { + bfd_set_state (bm, bs, BFD_STATE_down); + } + else + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_admin_down); + } + return 0; +} + +u8 * +bfd_input_format_trace (u8 * s, va_list * args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + const bfd_input_trace_t *t = va_arg (*args, bfd_input_trace_t *); + const bfd_pkt_t *pkt = (bfd_pkt_t *) t->data; + if (t->len > STRUCT_SIZE_OF (bfd_pkt_t, head)) + { + s = format (s, "BFD v%u, diag=%u(%s), state=%u(%s),\n" + " flags=(P:%u, F:%u, C:%u, A:%u, D:%u, M:%u), detect_mult=%u, " + "length=%u\n", + bfd_pkt_get_version (pkt), bfd_pkt_get_diag_code (pkt), + bfd_diag_code_string (bfd_pkt_get_diag_code (pkt)), + bfd_pkt_get_state (pkt), + bfd_state_string (bfd_pkt_get_state (pkt)), + bfd_pkt_get_poll (pkt), bfd_pkt_get_final (pkt), + bfd_pkt_get_control_plane_independent (pkt), + bfd_pkt_get_auth_present (pkt), bfd_pkt_get_demand (pkt), + bfd_pkt_get_multipoint (pkt), pkt->head.detect_mult, + pkt->head.length); + if (t->len >= sizeof (bfd_pkt_t) + && pkt->head.length >= sizeof (bfd_pkt_t)) + { + s = format (s, " my discriminator: %u\n", pkt->my_disc); + s = format (s, " your discriminator: %u\n", pkt->your_disc); + s = format (s, " desired min tx interval: %u\n", + clib_net_to_host_u32 (pkt->des_min_tx)); + s = format (s, " required min rx interval: %u\n", + clib_net_to_host_u32 (pkt->req_min_rx)); + s = format (s, " required min echo rx interval: %u\n", + clib_net_to_host_u32 (pkt->req_min_echo_rx)); + } + } + + return s; +} + +static void +bfd_on_state_change (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + BFD_DBG ("State changed: %U", format_bfd_session, bs); + bfd_event (bm, bs); + switch (bs->local_state) + { + case BFD_STATE_admin_down: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_down: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_init: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_up: + bfd_set_desired_min_tx (bm, bs, now, bs->config_desired_min_tx_us); + break; + } +} + +static void +bfd_add_transport_layer (vlib_main_t * vm, vlib_buffer_t * b, + bfd_session_t * bs) +{ + switch (bs->transport) + { + case BFD_TRANSPORT_UDP4: + /* fallthrough */ + case BFD_TRANSPORT_UDP6: + BFD_DBG ("Transport bfd via udp, bs_idx=%u", bs->bs_idx); + bfd_add_udp_transport (vm, b, &bs->udp); + break; + } +} + +static vlib_buffer_t * +bfd_create_frame (vlib_main_t * vm, vlib_node_runtime_t * rt, + bfd_session_t * bs) +{ + u32 bi; + if (vlib_buffer_alloc (vm, &bi, 1) != 1) + { + clib_warning ("buffer allocation failure"); + return NULL; + } + + vlib_buffer_t *b = vlib_get_buffer (vm, bi); + ASSERT (b->current_data == 0); + + u32 *to_next; + u32 n_left_to_next; + + vlib_get_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], + to_next, n_left_to_next); + + to_next[0] = bi; + n_left_to_next -= 1; + + vlib_put_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], + n_left_to_next); + return b; +} + +static void +bfd_init_control_frame (vlib_buffer_t * b, bfd_session_t * bs) +{ + bfd_pkt_t *pkt = vlib_buffer_get_current (b); + const u32 bfd_length = 24; + memset (pkt, 0, sizeof (*pkt)); + + bfd_pkt_set_version (pkt, 1); + bfd_pkt_set_diag_code (pkt, bs->local_diag); + bfd_pkt_set_state (pkt, bs->local_state); + if (bs->local_demand && BFD_STATE_up == bs->local_state && + BFD_STATE_up == bs->remote_state) + { + bfd_pkt_set_demand (pkt); + } + pkt->head.detect_mult = bs->local_detect_mult; + pkt->head.length = clib_host_to_net_u32 (bfd_length); + pkt->my_disc = bs->local_discr; + pkt->your_disc = bs->remote_discr; + pkt->des_min_tx = clib_host_to_net_u32 (bs->desired_min_tx_us); + pkt->req_min_rx = clib_host_to_net_u32 (bs->required_min_rx_us); + pkt->req_min_echo_rx = clib_host_to_net_u32 (0); /* FIXME */ + b->current_length = bfd_length; +} + +static void +bfd_send_periodic (vlib_main_t * vm, vlib_node_runtime_t * rt, + bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (!bs->remote_min_rx_us) + { + BFD_DBG + ("bfd.RemoteMinRxInterval is zero, not sending periodic control " + "frame"); + return; + } + /* FIXME + A system MUST NOT periodically transmit BFD Control packets if Demand + mode is active on the remote system (bfd.RemoteDemandMode is 1, + bfd.SessionState is Up, and bfd.RemoteSessionState is Up) and a Poll + Sequence is not being transmitted. + */ + if (now >= bs->tx_timeout_clocks) + { + BFD_DBG ("Send periodic control frame for bs_idx=%lu", bs->bs_idx); + vlib_buffer_t *b = bfd_create_frame (vm, rt, bs); + if (!b) + { + return; + } + bfd_init_control_frame (b, bs); + bfd_add_transport_layer (vm, b, bs); + bfd_calc_next_tx (bm, bs, now); + } + else + { + BFD_DBG ("No need to send control frame now"); + } + bfd_set_timer (bm, bs, now); +} + +void +bfd_send_final (vlib_main_t * vm, vlib_buffer_t * b, bfd_session_t * bs) +{ + BFD_DBG ("Send final control frame for bs_idx=%lu", bs->bs_idx); + bfd_init_control_frame (b, bs); + bfd_pkt_set_final (vlib_buffer_get_current (b)); + bfd_add_transport_layer (vm, b, bs); +} + +static void +bfd_check_rx_timeout (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (bs->last_rx_clocks + bs->detection_time_clocks < now) + { + BFD_DBG ("Rx timeout, session goes down"); + bfd_set_diag (bs, BFD_DIAG_CODE_det_time_exp); + bfd_set_state (bm, bs, BFD_STATE_down); + } +} + +void +bfd_on_timeout (vlib_main_t * vm, vlib_node_runtime_t * rt, bfd_main_t * bm, + bfd_session_t * bs, u64 now) +{ + BFD_DBG ("Timeout for bs_idx=%lu", bs->bs_idx); + switch (bs->local_state) + { + case BFD_STATE_admin_down: + BFD_ERR ("Unexpected timeout when in %s state", + bfd_state_string (bs->local_state)); + abort (); + break; + case BFD_STATE_down: + bfd_send_periodic (vm, rt, bm, bs, now); + break; + case BFD_STATE_init: + BFD_ERR ("Unexpected timeout when in %s state", + bfd_state_string (bs->local_state)); + abort (); + break; + case BFD_STATE_up: + bfd_check_rx_timeout (bm, bs, now); + bfd_send_periodic (vm, rt, bm, bs, now); + break; + } +} + +/* + * bfd input routine + */ +bfd_error_t +bfd_input (vlib_main_t * vm, vlib_buffer_t * b0, u32 bi0) +{ + // bfd_main_t *bm = &bfd_main; + bfd_error_t e; + + /* find our interface */ + bfd_session_t *s = NULL; + // bfd_get_intf (lm, vnet_buffer (b0)->sw_if_index[VLIB_RX]); + + if (!s) + { + /* bfd disabled on this interface, we're done */ + return BFD_ERROR_DISABLED; + } + + /* Actually scan the packet */ + e = BFD_ERROR_NONE; // bfd_packet_scan (lm, n, vlib_buffer_get_current (b0)); + + return e; +} + +/* + * bfd process node function + */ +static uword +bfd_process (vlib_main_t * vm, vlib_node_runtime_t * rt, vlib_frame_t * f) +{ + bfd_main_t *bm = &bfd_main; + u32 *expired = 0; + uword event_type, *event_data = 0; + + /* So we can send events to the bfd process */ + bm->bfd_process_node_index = bfd_process_node.index; + + while (1) + { + u64 now = clib_cpu_time_now (); + u64 next_expire = timing_wheel_next_expiring_elt_time (&bm->wheel); + BFD_DBG ("timing_wheel_next_expiring_elt_time(%p) returns %lu", + &bm->wheel, next_expire); + if ((i64) next_expire < 0) + { + BFD_DBG ("wait for event without timeout"); + (void) vlib_process_wait_for_event (vm); + } + else + { + f64 timeout = ((i64) next_expire - (i64) now) / bm->cpu_cps; + BFD_DBG ("wait for event with timeout %.02f", timeout); + ASSERT (timeout > 0); + (void) vlib_process_wait_for_event_or_clock (vm, timeout); + } + event_type = vlib_process_get_events (vm, &event_data); + now = clib_cpu_time_now (); + switch (event_type) + { + case ~0: /* no events => timeout */ + /* nothing to do here */ + break; + case BFD_EVENT_RESCHEDULE: + /* nothing to do here - reschedule is done automatically after + * each event or timeout */ + break; + case BFD_EVENT_NEW_SESSION: + do + { + bfd_session_t *bs = + pool_elt_at_index (bm->sessions, *event_data); + bfd_send_periodic (vm, rt, bm, bs, now); + } + while (0); + break; + default: + clib_warning ("BUG: event type 0x%wx", event_type); + break; + } + BFD_DBG ("advancing wheel, now is %lu", now); + BFD_DBG ("timing_wheel_advance (%p, %lu, %p, 0);", &bm->wheel, now, + expired); + expired = timing_wheel_advance (&bm->wheel, now, expired, 0); + BFD_DBG ("Expired %d elements", vec_len (expired)); + u32 *p = NULL; + vec_foreach (p, expired) + { + const u32 bs_idx = *p; + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + bs->wheel_time_clocks = 0; /* no longer scheduled */ + bfd_on_timeout (vm, rt, bm, bs, now); + } + } + if (expired) + { + _vec_len (expired) = 0; + } + if (event_data) + { + _vec_len (event_data) = 0; + } + } + + return 0; +} + +/* + * bfd process node declaration + */ +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_process_node, static) = { + .function = bfd_process, + .type = VLIB_NODE_TYPE_PROCESS, + .name = "bfd-process", + .n_next_nodes = BFD_OUTPUT_N_NEXT, + .next_nodes = + { +#define F(t, n) [BFD_OUTPUT_##t] = n, + foreach_bfd_transport (F) +#undef F + }, +}; +/* *INDENT-ON* */ + +static clib_error_t * +bfd_sw_interface_up_down (vnet_main_t * vnm, u32 sw_if_index, u32 flags) +{ + // bfd_main_t *bm = &bfd_main; + // vnet_hw_interface_t *hi = vnet_get_sup_hw_interface (vnm, sw_if_index); + if (!(flags & VNET_SW_INTERFACE_FLAG_ADMIN_UP)) + { + /* TODO */ + } + return 0; +} + +VNET_SW_INTERFACE_ADMIN_UP_DOWN_FUNCTION (bfd_sw_interface_up_down); + +static clib_error_t * +bfd_hw_interface_up_down (vnet_main_t * vnm, u32 hw_if_index, u32 flags) +{ + // bfd_main_t *bm = &bfd_main; + if (flags & VNET_HW_INTERFACE_FLAG_LINK_UP) + { + /* TODO */ + } + return 0; +} + +VNET_HW_INTERFACE_LINK_UP_DOWN_FUNCTION (bfd_hw_interface_up_down); + +/* + * setup function + */ +static clib_error_t * +bfd_main_init (vlib_main_t * vm) +{ + bfd_main_t *bm = &bfd_main; + bm->random_seed = random_default_seed (); + bm->vlib_main = vm; + bm->vnet_main = vnet_get_main (); + memset (&bm->wheel, 0, sizeof (bm->wheel)); + bm->cpu_cps = 2590000000; // vm->clib_time.clocks_per_second; + BFD_DBG ("cps is %.2f", bm->cpu_cps); + const u64 now = clib_cpu_time_now (); + timing_wheel_init (&bm->wheel, now, bm->cpu_cps); + + return 0; +} + +VLIB_INIT_FUNCTION (bfd_main_init); + +bfd_session_t * +bfd_get_session (bfd_main_t * bm, bfd_transport_t t) +{ + bfd_session_t *result; + pool_get (bm->sessions, result); + result->bs_idx = result - bm->sessions; + result->transport = t; + result->local_discr = random_u32 (&bm->random_seed); + bfd_set_defaults (bm, result); + hash_set (bm->session_by_disc, result->local_discr, result->bs_idx); + return result; +} + +void +bfd_put_session (bfd_main_t * bm, bfd_session_t * bs) +{ + hash_unset (bm->session_by_disc, bs->local_discr); + pool_put (bm->sessions, bs); +} + +bfd_session_t * +bfd_find_session_by_idx (bfd_main_t * bm, uword bs_idx) +{ + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + return pool_elt_at_index (bm->sessions, bs_idx); + } + return NULL; +} + +bfd_session_t * +bfd_find_session_by_disc (bfd_main_t * bm, u32 disc) +{ + uword *p = hash_get (bfd_main.session_by_disc, disc); + if (p) + { + return pool_elt_at_index (bfd_main.sessions, *p); + } + return NULL; +} + +/** + * @brief verify bfd packet - common checks + * + * @param pkt + * + * @return 1 if bfd packet is valid + */ +int +bfd_verify_pkt_common (const bfd_pkt_t * pkt) +{ + if (1 != bfd_pkt_get_version (pkt)) + { + BFD_ERR ("BFD verification failed - unexpected version: '%d'", + bfd_pkt_get_version (pkt)); + return 0; + } + if (pkt->head.length < sizeof (bfd_pkt_t) || + (bfd_pkt_get_auth_present (pkt) && + pkt->head.length < sizeof (bfd_pkt_with_auth_t))) + { + BFD_ERR ("BFD verification failed - unexpected length: '%d' (auth " + "present: %d)", + pkt->head.length, bfd_pkt_get_auth_present (pkt)); + return 0; + } + if (!pkt->head.detect_mult) + { + BFD_ERR ("BFD verification failed - unexpected detect-mult: '%d'", + pkt->head.detect_mult); + return 0; + } + if (bfd_pkt_get_multipoint (pkt)) + { + BFD_ERR ("BFD verification failed - unexpected multipoint: '%d'", + bfd_pkt_get_multipoint (pkt)); + return 0; + } + if (!pkt->my_disc) + { + BFD_ERR ("BFD verification failed - unexpected my-disc: '%d'", + pkt->my_disc); + return 0; + } + if (!pkt->your_disc) + { + const u8 pkt_state = bfd_pkt_get_state (pkt); + if (pkt_state != BFD_STATE_down && pkt_state != BFD_STATE_admin_down) + { + BFD_ERR ("BFD verification failed - unexpected state: '%s' " + "(your-disc is zero)", bfd_state_string (pkt_state)); + return 0; + } + } + return 1; +} + +/** + * @brief verify bfd packet - authentication + * + * @param pkt + * + * @return 1 if bfd packet is valid + */ +int +bfd_verify_pkt_session (const bfd_pkt_t * pkt, u16 pkt_size, + const bfd_session_t * bs) +{ + const bfd_pkt_with_auth_t *with_auth = (bfd_pkt_with_auth_t *) pkt; + if (!bfd_pkt_get_auth_present (pkt)) + { + if (pkt_size > sizeof (*pkt)) + { + BFD_ERR ("BFD verification failed - unexpected packet size '%d' " + "(auth not present)", pkt_size); + return 0; + } + } + else + { + if (!with_auth->auth.type) + { + BFD_ERR ("BFD verification failed - unexpected auth type: '%d'", + with_auth->auth.type); + return 0; + } + /* TODO FIXME - implement the actual verification */ + } + return 1; +} + +void +bfd_consume_pkt (bfd_main_t * bm, const bfd_pkt_t * pkt, u32 bs_idx) +{ + bfd_session_t *bs = bfd_find_session_by_idx (bm, bs_idx); + if (!bs) + { + return; + } + BFD_DBG ("Scanning bfd packet, bs_idx=%d", bs->bs_idx); + bs->remote_discr = pkt->my_disc; + bs->remote_state = bfd_pkt_get_state (pkt); + bs->remote_demand = bfd_pkt_get_demand (pkt); + bs->remote_min_rx_us = clib_net_to_host_u32 (pkt->req_min_rx); + bs->remote_min_rx_clocks = bfd_us_to_clocks (bm, bs->remote_min_rx_us); + BFD_DBG ("Set remote min rx to %lu clocks/%.2fs", bs->remote_min_rx_clocks, + bs->remote_min_rx_clocks / bm->cpu_cps); + bs->remote_desired_min_tx_us = clib_net_to_host_u32 (pkt->des_min_tx); + bs->remote_detect_mult = pkt->head.detect_mult; + bfd_recalc_detection_time (bm, bs); + bs->last_rx_clocks = clib_cpu_time_now (); + /* FIXME + If the Required Min Echo RX Interval field is zero, the + transmission of Echo packets, if any, MUST cease. + + If a Poll Sequence is being transmitted by the local system and + the Final (F) bit in the received packet is set, the Poll Sequence + MUST be terminated. + */ + /* FIXME 6.8.2 */ + /* FIXME 6.8.4 */ + if (BFD_STATE_admin_down == bs->local_state) + return; + if (BFD_STATE_admin_down == bs->remote_state) + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_down); + } + else if (BFD_STATE_down == bs->local_state) + { + if (BFD_STATE_down == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_init); + } + else if (BFD_STATE_init == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_up); + } + } + else if (BFD_STATE_init == bs->local_state) + { + if (BFD_STATE_up == bs->remote_state || + BFD_STATE_init == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_up); + } + } + else /* BFD_STATE_up == bs->local_state */ + { + if (BFD_STATE_down == bs->remote_state) + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_down); + } + } +} + +u8 * +format_bfd_session (u8 * s, va_list * args) +{ + const bfd_session_t *bs = va_arg (*args, bfd_session_t *); + return format (s, "BFD(%u): bfd.SessionState=%s, " + "bfd.RemoteSessionState=%s, " + "bfd.LocalDiscr=%u, " + "bfd.RemoteDiscr=%u, " + "bfd.LocalDiag=%s, " + "bfd.DesiredMinTxInterval=%u, " + "bfd.RequiredMinRxInterval=%u, " + "bfd.RemoteMinRxInterval=%u, " + "bfd.DemandMode=%s, " + "bfd.RemoteDemandMode=%s, " + "bfd.DetectMult=%u, ", + bs->bs_idx, bfd_state_string (bs->local_state), + bfd_state_string (bs->remote_state), bs->local_discr, + bs->remote_discr, bfd_diag_code_string (bs->local_diag), + bs->desired_min_tx_us, bs->required_min_rx_us, + bs->remote_min_rx_us, (bs->local_demand ? "yes" : "no"), + (bs->remote_demand ? "yes" : "no"), bs->local_detect_mult); +} + +bfd_main_t bfd_main; + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_main.h b/vnet/vnet/bfd/bfd_main.h new file mode 100644 index 00000000..727903bd --- /dev/null +++ b/vnet/vnet/bfd/bfd_main.h @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2011-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. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_main_h__ +#define __included_bfd_main_h__ + +#include +#include +#include +#include + +#define foreach_bfd_transport(F) \ + F (UDP4, "ip4-rewrite") \ + F (UDP6, "ip6-rewrite") + +typedef enum +{ +#define F(t, n) BFD_TRANSPORT_##t, + foreach_bfd_transport (F) +#undef F +} bfd_transport_t; + +#define foreach_bfd_mode(F) \ + F (asynchronous) \ + F (demand) + +typedef enum +{ +#define F(x) BFD_MODE_##x, + foreach_bfd_mode (F) +#undef F +} bfd_mode_e; + +typedef struct +{ + /* index in bfd_main.sessions pool */ + uword bs_idx; + + /* session state */ + bfd_state_e local_state; + + /* local diagnostics */ + bfd_diag_code_e local_diag; + + /* remote session state */ + bfd_state_e remote_state; + + /* local discriminator */ + u32 local_discr; + + /* remote discriminator */ + u32 remote_discr; + + /* configured desired min tx interval (microseconds) */ + u32 config_desired_min_tx_us; + + /* desired min tx interval (microseconds) */ + u32 desired_min_tx_us; + + /* desired min tx interval (clocks) */ + u64 desired_min_tx_clocks; + + /* required min rx interval */ + u32 required_min_rx_us; + + /* remote min rx interval (microseconds) */ + u32 remote_min_rx_us; + + /* remote min rx interval (clocks) */ + u64 remote_min_rx_clocks; + + /* remote desired min tx interval */ + u32 remote_desired_min_tx_us; + + /* 1 if in demand mode, 0 otherwise */ + u8 local_demand; + + /* 1 if remote system sets demand mode, 0 otherwise */ + u8 remote_demand; + + u8 local_detect_mult; + u8 remote_detect_mult; + + /* set to value of timer in timing wheel, 0 if not set */ + u64 wheel_time_clocks; + + /* transmit interval */ + u64 transmit_interval_clocks; + + /* next time at which to transmit a packet */ + u64 tx_timeout_clocks; + + /* timestamp of last packet received */ + u64 last_rx_clocks; + + /* detection time */ + u64 detection_time_clocks; + + /* transport type for this session */ + bfd_transport_t transport; + + union + { + bfd_udp_session_t udp; + }; +} bfd_session_t; + +typedef struct +{ + u32 client_index; + u32 client_pid; +} event_subscriber_t; + +typedef struct +{ + /* pool of bfd sessions context data */ + bfd_session_t *sessions; + + /* timing wheel for scheduling timeouts */ + timing_wheel_t wheel; + + /* hashmap - bfd session by discriminator */ + u32 *session_by_disc; + + /* background process node index */ + u32 bfd_process_node_index; + + /* convenience variables */ + vlib_main_t *vlib_main; + vnet_main_t *vnet_main; + + /* cpu clocks per second */ + f64 cpu_cps; + + /* for generating random numbers */ + u32 random_seed; + + /* pool of event subscribers */ + //event_subscriber_t *subscribers; + +} bfd_main_t; + +extern bfd_main_t bfd_main; + +/* Packet counters */ +#define foreach_bfd_error(F) \ + F (NONE, "good bfd packets (processed)") \ + F (BAD, "invalid bfd packets") \ + F (DISABLED, "bfd packets received on disabled interfaces") + +typedef enum +{ +#define F(sym, str) BFD_ERROR_##sym, + foreach_bfd_error (F) +#undef F + BFD_N_ERROR, +} bfd_error_t; + +/* bfd packet trace capture */ +typedef struct +{ + u32 len; + u8 data[400]; +} bfd_input_trace_t; + +enum +{ + BFD_EVENT_RESCHEDULE = 1, + BFD_EVENT_NEW_SESSION, +} bfd_process_event_e; + +bfd_error_t bfd_input (vlib_main_t * vm, vlib_buffer_t * b0, u32 bi0); +u8 *bfd_input_format_trace (u8 * s, va_list * args); + +bfd_session_t *bfd_get_session (bfd_main_t * bm, bfd_transport_t t); +void bfd_put_session (bfd_main_t * bm, bfd_session_t * bs); +bfd_session_t *bfd_find_session_by_idx (bfd_main_t * bm, uword bs_idx); +bfd_session_t *bfd_find_session_by_disc (bfd_main_t * bm, u32 disc); +void bfd_session_start (bfd_main_t * bm, bfd_session_t * bs); +void bfd_consume_pkt (bfd_main_t * bm, const bfd_pkt_t * bfd, u32 bs_idx); +int bfd_verify_pkt_common (const bfd_pkt_t * pkt); +int bfd_verify_pkt_session (const bfd_pkt_t * pkt, u16 pkt_size, + const bfd_session_t * bs); +void bfd_event (bfd_main_t * bm, bfd_session_t * bs); +void bfd_send_final (vlib_main_t * vm, vlib_buffer_t * b, bfd_session_t * bs); +u8 *format_bfd_session (u8 * s, va_list * args); + + +#define USEC_PER_MS 1000LL +#define USEC_PER_SECOND (1000 * USEC_PER_MS) + +/* default, slow transmission interval for BFD packets, per spec at least 1s */ +#define BFD_DEFAULT_DESIRED_MIN_TX_US USEC_PER_SECOND + +#endif /* __included_bfd_main_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_protocol.c b/vnet/vnet/bfd/bfd_protocol.c new file mode 100644 index 00000000..ede9536f --- /dev/null +++ b/vnet/vnet/bfd/bfd_protocol.c @@ -0,0 +1,74 @@ +#include + +u8 bfd_pkt_get_version (const bfd_pkt_t *pkt) +{ + return pkt->head.vers_diag >> 5; +} + +void bfd_pkt_set_version (bfd_pkt_t *pkt, int version) +{ + pkt->head.vers_diag = + (version << 5) | (pkt->head.vers_diag & ((1 << 5) - 1)); +} + +u8 bfd_pkt_get_diag_code (const bfd_pkt_t *pkt) +{ + return pkt->head.vers_diag & ((1 << 5) - 1); +} + +void bfd_pkt_set_diag_code (bfd_pkt_t *pkt, int value) +{ + pkt->head.vers_diag = + (pkt->head.vers_diag & ~((1 << 5) - 1)) | (value & ((1 << 5) - 1)); +} + +u8 bfd_pkt_get_state (const bfd_pkt_t *pkt) +{ + return pkt->head.sta_flags >> 6; +} + +void bfd_pkt_set_state (bfd_pkt_t *pkt, int value) +{ + pkt->head.sta_flags = (value << 6) | (pkt->head.sta_flags & ((1 << 6) - 1)); +} + +u8 bfd_pkt_get_poll (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 5) & 1; +} + +void bfd_pkt_set_final (bfd_pkt_t *pkt) { pkt->head.sta_flags |= 1 << 5; } + +u8 bfd_pkt_get_final (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 4) & 1; +} + +void bfd_pkt_set_poll (bfd_pkt_t *pkt); +u8 bfd_pkt_get_control_plane_independent (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 3) & 1; +} + +void bfd_pkt_set_control_plane_independent (bfd_pkt_t *pkt); + +u8 bfd_pkt_get_auth_present (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 2) & 1; +} + +void bfd_pkt_set_auth_present (bfd_pkt_t *pkt); + +u8 bfd_pkt_get_demand (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 1) & 1; +} + +void bfd_pkt_set_demand (bfd_pkt_t *pkt) { pkt->head.sta_flags |= 1 << 1; } + +u8 bfd_pkt_get_multipoint (const bfd_pkt_t *pkt) +{ + return pkt->head.sta_flags & 1; +} + +void bfd_pkt_set_multipoint (bfd_pkt_t *pkt); diff --git a/vnet/vnet/bfd/bfd_protocol.h b/vnet/vnet/bfd/bfd_protocol.h new file mode 100644 index 00000000..cf751b3b --- /dev/null +++ b/vnet/vnet/bfd/bfd_protocol.h @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2011-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. + */ +#ifndef __included_bfd_protocol_h__ +#define __included_bfd_protocol_h__ +/** + * @file + * @brief BFD protocol declarations + */ + +#include +#include + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + /* + An optional Authentication Section MAY be present: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Auth Type | Auth Len | Authentication Data... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + u8 type; + u8 len; + u8 data[0]; +}) bfd_auth_t; +/* *INDENT-ON* */ + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + /* + The Mandatory Section of a BFD Control packet has the following + format: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Vers | Diag |Sta|P|F|C|A|D|M| Detect Mult | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | My Discriminator | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Your Discriminator | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Desired Min TX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Required Min RX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Required Min Echo RX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + struct + { + u8 vers_diag; + u8 sta_flags; + u8 detect_mult; + u8 length; + } head; + u32 my_disc; + u32 your_disc; + u32 des_min_tx; + u32 req_min_rx; + u32 req_min_echo_rx; +}) bfd_pkt_t; +/* *INDENT-ON* */ + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + bfd_pkt_t pkt; + bfd_auth_t auth; +}) bfd_pkt_with_auth_t; +/* *INDENT-ON* */ + +u8 bfd_pkt_get_version (const bfd_pkt_t * pkt); +void bfd_pkt_set_version (bfd_pkt_t * pkt, int version); +u8 bfd_pkt_get_diag_code (const bfd_pkt_t * pkt); +void bfd_pkt_set_diag_code (bfd_pkt_t * pkt, int value); +u8 bfd_pkt_get_state (const bfd_pkt_t * pkt); +void bfd_pkt_set_state (bfd_pkt_t * pkt, int value); +u8 bfd_pkt_get_poll (const bfd_pkt_t * pkt); +void bfd_pkt_set_final (bfd_pkt_t * pkt); +u8 bfd_pkt_get_final (const bfd_pkt_t * pkt); +void bfd_pkt_set_poll (bfd_pkt_t * pkt); +u8 bfd_pkt_get_control_plane_independent (const bfd_pkt_t * pkt); +void bfd_pkt_set_control_plane_independent (bfd_pkt_t * pkt); +u8 bfd_pkt_get_auth_present (const bfd_pkt_t * pkt); +void bfd_pkt_set_auth_present (bfd_pkt_t * pkt); +u8 bfd_pkt_get_demand (const bfd_pkt_t * pkt); +void bfd_pkt_set_demand (bfd_pkt_t * pkt); +u8 bfd_pkt_get_multipoint (const bfd_pkt_t * pkt); +void bfd_pkt_set_multipoint (bfd_pkt_t * pkt); + +/* BFD diagnostic codes */ +#define foreach_bfd_diag_code(F) \ + F (0, no_diag, "No Diagnostic") \ + F (1, det_time_exp, "Control Detection Time Expired") \ + F (2, echo_failed, "Echo Function Failed") \ + F (3, neighbor_sig_down, "Neighbor Signaled Session Down") \ + F (4, fwd_plain_reset, "Forwarding Plane Reset") \ + F (5, path_down, "Path Down") \ + F (6, concat_path_down, "Concatenated Path Down") \ + F (7, admin_down, "Administratively Down") \ + F (8, reverse_concat_path_down, "Reverse Concatenated Path Down") + +#define BFD_DIAG_CODE_NAME(t) BFD_DIAG_CODE_##t + +typedef enum +{ +#define F(n, t, s) BFD_DIAG_CODE_NAME (t) = n, + foreach_bfd_diag_code (F) +#undef F +} bfd_diag_code_e; + +const char *bfd_diag_code_string (bfd_diag_code_e diag); + +/* BFD state values */ +#define foreach_bfd_state(F) \ + F (0, admin_down, "AdminDown") \ + F (1, down, "Down") \ + F (2, init, "Init") \ + F (3, up, "Up") + +#define BFD_STATE_NAME(t) BFD_STATE_##t + +typedef enum +{ +#define F(n, t, s) BFD_STATE_NAME (t) = n, + foreach_bfd_state (F) +#undef F +} bfd_state_e; + +const char *bfd_state_string (bfd_state_e state); + +#endif /* __included_bfd_protocol_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_udp.c b/vnet/vnet/bfd/bfd_udp.c new file mode 100644 index 00000000..ded33425 --- /dev/null +++ b/vnet/vnet/bfd/bfd_udp.c @@ -0,0 +1,610 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + bfd_main_t *bfd_main; + /* hashmap - bfd session index by bfd key - used for CLI/API lookup, where + * discriminator is unknown */ + mhash_t bfd_session_idx_by_bfd_key; +} bfd_udp_main_t; + +bfd_udp_main_t bfd_udp_main; + +void bfd_udp_transport_to_buffer (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) +{ + udp_header_t *udp; + u16 udp_length, ip_length; + bfd_udp_key_t *key = &bus->key; + + b->flags |= VNET_BUFFER_LOCALLY_ORIGINATED; + if (ip46_address_is_ip4 (&key->local_addr)) + { + ip4_header_t *ip4; + const size_t data_size = sizeof (*ip4) + sizeof (*udp); + vlib_buffer_advance (b, -data_size); + ip4 = vlib_buffer_get_current (b); + udp = (udp_header_t *)(ip4 + 1); + memset (ip4, 0, data_size); + ip4->ip_version_and_header_length = 0x45; + ip4->ttl = 255; + ip4->protocol = IP_PROTOCOL_UDP; + ip4->src_address.as_u32 = key->local_addr.ip4.as_u32; + ip4->dst_address.as_u32 = key->peer_addr.ip4.as_u32; + + udp->src_port = clib_host_to_net_u16 (50000); /* FIXME */ + udp->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_bfd4); + + /* fix ip length, checksum and udp length */ + ip_length = vlib_buffer_length_in_chain (vm, b); + + ip4->length = clib_host_to_net_u16 (ip_length); + ip4->checksum = ip4_header_checksum (ip4); + + udp_length = ip_length - (sizeof (*ip4)); + udp->length = clib_host_to_net_u16 (udp_length); + } + else + { + BFD_ERR ("not implemented"); + abort (); + } +} + +void bfd_add_udp_transport (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) +{ + vnet_buffer (b)->ip.adj_index[VLIB_RX] = bus->adj_index; + vnet_buffer (b)->ip.adj_index[VLIB_TX] = bus->adj_index; + bfd_udp_transport_to_buffer (vm, b, bus); +} + +static bfd_session_t *bfd_lookup_session (bfd_udp_main_t *bum, + const bfd_udp_key_t *key) +{ + uword *p = mhash_get (&bum->bfd_session_idx_by_bfd_key, key); + if (p) + { + return bfd_find_session_by_idx (bum->bfd_main, *p); + } + return 0; +} + +static vnet_api_error_t +bfd_udp_add_session_internal (bfd_udp_main_t *bum, u32 sw_if_index, + u32 desired_min_tx_us, u32 required_min_rx_us, + u8 detect_mult, const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + /* get a pool entry and if we end up not needing it, give it back */ + bfd_transport_t t = BFD_TRANSPORT_UDP4; + if (!ip46_address_is_ip4 (local_addr)) + { + t = BFD_TRANSPORT_UDP6; + } + bfd_session_t *bs = bfd_get_session (bum->bfd_main, t); + bfd_udp_session_t *bus = &bs->udp; + memset (bus, 0, sizeof (*bus)); + bfd_udp_key_t *key = &bus->key; + key->sw_if_index = sw_if->sw_if_index; + key->local_addr.as_u64[0] = local_addr->as_u64[0]; + key->local_addr.as_u64[1] = local_addr->as_u64[1]; + key->peer_addr.as_u64[0] = peer_addr->as_u64[0]; + key->peer_addr.as_u64[1] = peer_addr->as_u64[1]; + const bfd_session_t *tmp = bfd_lookup_session (bum, key); + if (tmp) + { + BFD_ERR ("duplicate bfd-udp session, existing bs_idx=%d", tmp->bs_idx); + bfd_put_session (bum->bfd_main, bs); + return VNET_API_ERROR_BFD_EEXIST; + } + key->sw_if_index = sw_if->sw_if_index; + mhash_set (&bum->bfd_session_idx_by_bfd_key, key, bs->bs_idx, NULL); + BFD_DBG ("session created, bs_idx=%u, sw_if_index=%d, local=%U, peer=%U", + bs->bs_idx, key->sw_if_index, format_ip46_address, &key->local_addr, + IP46_TYPE_ANY, format_ip46_address, &key->peer_addr, IP46_TYPE_ANY); + if (BFD_TRANSPORT_UDP4 == t) + { + bus->adj_index = adj_nbr_add_or_lock (FIB_PROTOCOL_IP4, VNET_LINK_IP4, + &key->peer_addr, key->sw_if_index); + BFD_DBG ("adj_nbr_add_or_lock(FIB_PROTOCOL_IP4, VNET_LINK_IP4, %U, %d) " + "returns %d", + format_ip46_address, &key->peer_addr, IP46_TYPE_ANY, + key->sw_if_index, bus->adj_index); + } + else + { + bus->adj_index = adj_nbr_add_or_lock (FIB_PROTOCOL_IP6, VNET_LINK_IP6, + &key->peer_addr, key->sw_if_index); + BFD_DBG ("adj_nbr_add_or_lock(FIB_PROTOCOL_IP6, VNET_LINK_IP6, %U, %d) " + "returns %d", + format_ip46_address, &key->peer_addr, IP46_TYPE_ANY, + key->sw_if_index, bus->adj_index); + } + bs->config_desired_min_tx_us = desired_min_tx_us; + bs->required_min_rx_us = required_min_rx_us; + bs->local_detect_mult = detect_mult; + bfd_session_start (bum->bfd_main, bs); + return 0; +} + +static vnet_api_error_t +bfd_udp_validate_api_input (u32 sw_if_index, const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + u8 local_ip_valid = 0; + ip_interface_address_t *ia = NULL; + if (!sw_if) + { + BFD_ERR ("got NULL sw_if"); + return VNET_API_ERROR_INVALID_SW_IF_INDEX; + } + if (ip46_address_is_ip4 (local_addr)) + { + if (!ip46_address_is_ip4 (peer_addr)) + { + BFD_ERR ("IP family mismatch"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + ip4_main_t *im = &ip4_main; + + /* *INDENT-OFF* */ + foreach_ip_interface_address ( + &im->lookup_main, ia, sw_if_index, 0 /* honor unnumbered */, ({ + ip4_address_t *x = + ip_interface_address_get_address (&im->lookup_main, ia); + if (x->as_u32 == local_addr->ip4.as_u32) + { + /* valid address for this interface */ + local_ip_valid = 1; + break; + } + })); + /* *INDENT-ON* */ + } + else + { + if (ip46_address_is_ip4 (peer_addr)) + { + BFD_ERR ("IP family mismatch"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + ip6_main_t *im = &ip6_main; + /* *INDENT-OFF* */ + foreach_ip_interface_address ( + &im->lookup_main, ia, sw_if_index, 0 /* honor unnumbered */, ({ + ip6_address_t *x = + ip_interface_address_get_address (&im->lookup_main, ia); + if (local_addr->ip6.as_u64[0] == x->as_u64[0] && + local_addr->ip6.as_u64[1] == x->as_u64[1]) + { + /* valid address for this interface */ + local_ip_valid = 1; + break; + } + })); + /* *INDENT-ON* */ + } + + if (!local_ip_valid) + { + BFD_ERR ("address not found on interface"); + return VNET_API_ERROR_ADDRESS_NOT_FOUND_FOR_INTERFACE; + } + + return 0; +} + +vnet_api_error_t bfd_udp_add_session (u32 sw_if_index, u32 desired_min_tx_us, + u32 required_min_rx_us, u8 detect_mult, + const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_api_error_t rv = + bfd_udp_validate_api_input (sw_if_index, local_addr, peer_addr); + if (rv) + { + return rv; + } + if (detect_mult < 1) + { + BFD_ERR ("detect_mult < 1"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + if (desired_min_tx_us < 1) + { + BFD_ERR ("desired_min_tx_us < 1"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + return bfd_udp_add_session_internal (&bfd_udp_main, sw_if_index, + desired_min_tx_us, required_min_rx_us, + detect_mult, local_addr, peer_addr); +} + +vnet_api_error_t bfd_udp_del_session (u32 sw_if_index, + const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_api_error_t rv = + bfd_udp_validate_api_input (sw_if_index, local_addr, peer_addr); + if (rv) + { + return rv; + } + bfd_udp_main_t *bum = &bfd_udp_main; + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + bfd_udp_key_t key; + memset (&key, 0, sizeof (key)); + key.sw_if_index = sw_if->sw_if_index; + key.local_addr.as_u64[0] = local_addr->as_u64[0]; + key.local_addr.as_u64[1] = local_addr->as_u64[1]; + key.peer_addr.as_u64[0] = peer_addr->as_u64[0]; + key.peer_addr.as_u64[1] = peer_addr->as_u64[1]; + bfd_session_t *tmp = bfd_lookup_session (bum, &key); + if (tmp) + { + BFD_DBG ("free bfd-udp session, bs_idx=%d", tmp->bs_idx); + mhash_unset (&bum->bfd_session_idx_by_bfd_key, &key, NULL); + adj_unlock (tmp->udp.adj_index); + bfd_put_session (bum->bfd_main, tmp); + } + else + { + BFD_ERR ("no such session"); + return VNET_API_ERROR_BFD_NOENT; + } + return 0; +} + +typedef enum { + BFD_UDP_INPUT_NEXT_NORMAL, + BFD_UDP_INPUT_NEXT_REPLY, + BFD_UDP_INPUT_N_NEXT, +} bfd_udp_input_next_t; + +/* Packet counters */ +#define foreach_bfd_udp_error(F) \ + F (NONE, "good bfd packets (processed)") \ + F (BAD, "invalid bfd packets") \ + F (DISABLED, "bfd packets received on disabled interfaces") + +#define F(sym, string) static char BFD_UDP_ERR_##sym##_STR[] = string; +foreach_bfd_udp_error (F); +#undef F + +static char *bfd_udp_error_strings[] = { +#define F(sym, string) BFD_UDP_ERR_##sym##_STR, + foreach_bfd_udp_error (F) +#undef F +}; + +typedef enum { +#define F(sym, str) BFD_UDP_ERROR_##sym, + foreach_bfd_udp_error (F) +#undef F + BFD_UDP_N_ERROR, +} bfd_udp_error_t; + +static void bfd_udp4_find_headers (vlib_buffer_t *b, const ip4_header_t **ip4, + const udp_header_t **udp) +{ + *ip4 = vnet_buffer (b)->ip.header; + *udp = (udp_header_t *)((*ip4) + 1); +} + +static bfd_udp_error_t bfd_udp4_verify_transport (const ip4_header_t *ip4, + const udp_header_t *udp, + const bfd_session_t *bs) +{ + const bfd_udp_session_t *bus = &bs->udp; + const bfd_udp_key_t *key = &bus->key; + if (ip4->src_address.as_u32 != key->peer_addr.ip4.as_u32) + { + BFD_ERR ("IP src addr mismatch, got %U, expected %U", format_ip4_address, + ip4->src_address.as_u32, format_ip4_address, + key->peer_addr.ip4.as_u32); + return BFD_UDP_ERROR_BAD; + } + if (ip4->dst_address.as_u32 != key->local_addr.ip4.as_u32) + { + BFD_ERR ("IP dst addr mismatch, got %U, expected %U", format_ip4_address, + ip4->dst_address.as_u32, format_ip4_address, + key->local_addr.ip4.as_u32); + return BFD_UDP_ERROR_BAD; + } + const u8 expected_ttl = 255; + if (ip4->ttl != expected_ttl) + { + BFD_ERR ("IP unexpected TTL value %d, expected %d", ip4->ttl, + expected_ttl); + return BFD_UDP_ERROR_BAD; + } + if (clib_net_to_host_u16 (udp->src_port) < 49152 || + clib_net_to_host_u16 (udp->src_port) > 65535) + { + BFD_ERR ("Invalid UDP src port %d, out of range <49152,65535>", + udp->src_port); + } + return BFD_UDP_ERROR_NONE; +} + +typedef struct +{ + u32 bs_idx; + bfd_pkt_t pkt; +} bfd_rpc_update_t; + +static void bfd_rpc_update_session_cb (const bfd_rpc_update_t *a) +{ + bfd_consume_pkt (bfd_udp_main.bfd_main, &a->pkt, a->bs_idx); +} + +static void bfd_rpc_update_session (u32 bs_idx, const bfd_pkt_t *pkt) +{ + /* packet length was already verified to be correct by the caller */ + const u32 data_size = sizeof (bfd_rpc_update_t) - + STRUCT_SIZE_OF (bfd_rpc_update_t, pkt) + + pkt->head.length; + u8 data[data_size]; + bfd_rpc_update_t *update = (bfd_rpc_update_t *)data; + update->bs_idx = bs_idx; + clib_memcpy (&update->pkt, pkt, pkt->head.length); + vl_api_rpc_call_main_thread (bfd_rpc_update_session_cb, data, data_size); +} + +static bfd_udp_error_t bfd_udp4_scan (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_buffer_t *b, bfd_session_t **bs_out) +{ + const bfd_pkt_t *pkt = vlib_buffer_get_current (b); + if (sizeof (*pkt) > b->current_length) + { + BFD_ERR ( + "Payload size %d too small to hold bfd packet of minimum size %d", + b->current_length, sizeof (*pkt)); + return BFD_UDP_ERROR_BAD; + } + const ip4_header_t *ip4; + const udp_header_t *udp; + bfd_udp4_find_headers (b, &ip4, &udp); + if (!ip4 || !udp) + { + BFD_ERR ("Couldn't find ip4 or udp header"); + return BFD_UDP_ERROR_BAD; + } + if (!bfd_verify_pkt_common (pkt)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_session_t *bs = NULL; + if (pkt->your_disc) + { + BFD_DBG ("Looking up BFD session using discriminator %u", + pkt->your_disc); + bs = bfd_find_session_by_disc (bfd_udp_main.bfd_main, pkt->your_disc); + } + else + { + bfd_udp_key_t key; + memset (&key, 0, sizeof (key)); + key.sw_if_index = vnet_buffer (b)->sw_if_index[VLIB_RX]; + key.local_addr.ip4.as_u32 = ip4->dst_address.as_u32; + key.peer_addr.ip4.as_u32 = ip4->src_address.as_u32; + BFD_DBG ("Looking up BFD session using key (sw_if_index=%u, local=%U, " + "peer=%U)", + key.sw_if_index, format_ip4_address, key.local_addr.ip4.as_u8, + format_ip4_address, key.peer_addr.ip4.as_u8); + bs = bfd_lookup_session (&bfd_udp_main, &key); + } + if (!bs) + { + BFD_ERR ("BFD session lookup failed - no session matches BFD pkt"); + return BFD_UDP_ERROR_BAD; + } + BFD_DBG ("BFD session found, bs_idx=%d", bs->bs_idx); + if (!bfd_verify_pkt_session (pkt, b->current_length, bs)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_udp_error_t err; + if (BFD_UDP_ERROR_NONE != (err = bfd_udp4_verify_transport (ip4, udp, bs))) + { + return err; + } + bfd_rpc_update_session (bs->bs_idx, pkt); + *bs_out = bs; + return BFD_UDP_ERROR_NONE; +} + +static bfd_udp_error_t bfd_udp6_scan (vlib_main_t *vm, vlib_buffer_t *b) +{ + /* TODO */ + return BFD_UDP_ERROR_BAD; +} + +/* + * Process a frame of bfd packets + * Expect 1 packet / frame + */ +static uword bfd_udp_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f, int is_ipv6) +{ + u32 n_left_from, *from; + bfd_input_trace_t *t0; + + from = vlib_frame_vector_args (f); /* array of buffer indices */ + n_left_from = f->n_vectors; /* number of buffer indices */ + + while (n_left_from > 0) + { + u32 bi0; + vlib_buffer_t *b0; + u32 next0, error0; + + bi0 = from[0]; + b0 = vlib_get_buffer (vm, bi0); + + bfd_session_t *bs = NULL; + + /* If this pkt is traced, snapshot the data */ + if (b0->flags & VLIB_BUFFER_IS_TRACED) + { + int len; + t0 = vlib_add_trace (vm, rt, b0, sizeof (*t0)); + len = (b0->current_length < sizeof (t0->data)) ? b0->current_length + : sizeof (t0->data); + t0->len = len; + clib_memcpy (t0->data, vlib_buffer_get_current (b0), len); + } + + /* scan this bfd pkt. error0 is the counter index to bmp */ + if (is_ipv6) + { + error0 = bfd_udp6_scan (vm, b0); + } + else + { + error0 = bfd_udp4_scan (vm, rt, b0, &bs); + } + b0->error = rt->errors[error0]; + + next0 = BFD_UDP_INPUT_NEXT_NORMAL; + if (BFD_UDP_ERROR_NONE == error0) + { + /* if everything went fine, check for poll bit, if present, re-use + the buffer and based on (now update) session parameters, send the + final packet back */ + const bfd_pkt_t *pkt = vlib_buffer_get_current (b0); + if (bfd_pkt_get_poll (pkt)) + { + bfd_send_final (vm, b0, bs); + next0 = BFD_UDP_INPUT_NEXT_REPLY; + } + } + vlib_set_next_frame_buffer (vm, rt, next0, bi0); + + from += 1; + n_left_from -= 1; + } + + return f->n_vectors; +} + +static uword bfd_udp4_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f) +{ + return bfd_udp_input (vm, rt, f, 0); +} + +/* + * bfd input graph node declaration + */ +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_udp4_input_node, static) = { + .function = bfd_udp4_input, + .name = "bfd-udp4-input", + .vector_size = sizeof (u32), + .type = VLIB_NODE_TYPE_INTERNAL, + + .n_errors = BFD_UDP_N_ERROR, + .error_strings = bfd_udp_error_strings, + + .format_trace = bfd_input_format_trace, + + .n_next_nodes = BFD_UDP_INPUT_N_NEXT, + .next_nodes = + { + [BFD_UDP_INPUT_NEXT_NORMAL] = "error-drop", + [BFD_UDP_INPUT_NEXT_REPLY] = "ip4-lookup", + }, +}; +/* *INDENT-ON* */ + +static uword bfd_udp6_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f) +{ + return bfd_udp_input (vm, rt, f, 1); +} + +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_udp6_input_node, static) = { + .function = bfd_udp6_input, + .name = "bfd-udp6-input", + .vector_size = sizeof (u32), + .type = VLIB_NODE_TYPE_INTERNAL, + + .n_errors = BFD_UDP_N_ERROR, + .error_strings = bfd_udp_error_strings, + + .format_trace = bfd_input_format_trace, + + .n_next_nodes = BFD_UDP_INPUT_N_NEXT, + .next_nodes = + { + [BFD_UDP_INPUT_NEXT_NORMAL] = "error-drop", + [BFD_UDP_INPUT_NEXT_REPLY] = "ip6-lookup", + }, +}; +/* *INDENT-ON* */ + +static clib_error_t *bfd_sw_interface_up_down (vnet_main_t *vnm, + u32 sw_if_index, u32 flags) +{ + // vnet_hw_interface_t *hi = vnet_get_sup_hw_interface (vnm, sw_if_index); + if (!(flags & VNET_SW_INTERFACE_FLAG_ADMIN_UP)) + { + /* TODO */ + } + return 0; +} + +VNET_SW_INTERFACE_ADMIN_UP_DOWN_FUNCTION (bfd_sw_interface_up_down); + +static clib_error_t *bfd_hw_interface_up_down (vnet_main_t *vnm, + u32 hw_if_index, u32 flags) +{ + if (flags & VNET_HW_INTERFACE_FLAG_LINK_UP) + { + /* TODO */ + } + return 0; +} + +VNET_HW_INTERFACE_LINK_UP_DOWN_FUNCTION (bfd_hw_interface_up_down); + +/* + * setup function + */ +static clib_error_t *bfd_udp_init (vlib_main_t *vm) +{ + mhash_init (&bfd_udp_main.bfd_session_idx_by_bfd_key, sizeof (uword), + sizeof (bfd_udp_key_t)); + bfd_udp_main.bfd_main = &bfd_main; + udp_register_dst_port (vm, UDP_DST_PORT_bfd4, bfd_udp4_input_node.index, 1); + udp_register_dst_port (vm, UDP_DST_PORT_bfd6, bfd_udp6_input_node.index, 0); + return 0; +} + +VLIB_INIT_FUNCTION (bfd_udp_init); diff --git a/vnet/vnet/bfd/bfd_udp.h b/vnet/vnet/bfd/bfd_udp.h new file mode 100644 index 00000000..51f5327b --- /dev/null +++ b/vnet/vnet/bfd/bfd_udp.h @@ -0,0 +1,56 @@ +/* * Copyright (c) 2011-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. + */ +/** + * @file + * @brief BFD global declarations + */ + +#ifndef __included_bfd_udp_h__ +#define __included_bfd_udp_h__ + +#include +#include +#include + +#define BFD_UDP_KEY_BODY + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + + u32 sw_if_index; + ip46_address_t local_addr; + ip46_address_t peer_addr; + +}) bfd_udp_key_t; +/* *INDENT-ON* */ + +typedef struct +{ + bfd_udp_key_t key; + + adj_index_t adj_index; +} bfd_udp_session_t; + +void bfd_add_udp_transport (vlib_main_t * vm, vlib_buffer_t * b, + bfd_udp_session_t * bs); + +#endif /* __included_bfd_udp_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/dir.dox b/vnet/vnet/bfd/dir.dox new file mode 100644 index 00000000..ed656b52 --- /dev/null +++ b/vnet/vnet/bfd/dir.dox @@ -0,0 +1,18 @@ +/* + * 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. + */ +/** + @dir vnet/vnet/bfd + @brief Bidirectional Forwarding Detection (BFD) implementation +*/ diff --git a/vnet/vnet/buffer.h b/vnet/vnet/buffer.h index 898c94ee..82806bba 100644 --- a/vnet/vnet/buffer.h +++ b/vnet/vnet/buffer.h @@ -143,7 +143,11 @@ typedef struct u8 code; u32 data; } icmp; + + /* IP header - saved by ip*_local nodes */ + void *header; }; + } ip; /* diff --git a/vnet/vnet/ip/ip4_forward.c b/vnet/vnet/ip/ip4_forward.c index fc7b3496..38729c8e 100644 --- a/vnet/vnet/ip/ip4_forward.c +++ b/vnet/vnet/ip/ip4_forward.c @@ -1472,9 +1472,12 @@ ip4_local (vlib_main_t * vm, ip0 = vlib_buffer_get_current (p0); ip1 = vlib_buffer_get_current (p1); - fib_index0 = vec_elt (im->fib_index_by_sw_if_index, - vnet_buffer(p0)->sw_if_index[VLIB_RX]); - fib_index1 = vec_elt (im->fib_index_by_sw_if_index, + vnet_buffer (p0)->ip.header = ip0; + vnet_buffer (p1)->ip.header = ip1; + + fib_index0 = vec_elt (im->fib_index_by_sw_if_index, + vnet_buffer (p0)->sw_if_index[VLIB_RX]); + fib_index1 = vec_elt (im->fib_index_by_sw_if_index, vnet_buffer(p1)->sw_if_index[VLIB_RX]); mtrie0 = &ip4_fib_get (fib_index0)->mtrie; @@ -1679,6 +1682,8 @@ ip4_local (vlib_main_t * vm, ip0 = vlib_buffer_get_current (p0); + vnet_buffer (p0)->ip.header = ip0; + fib_index0 = vec_elt (im->fib_index_by_sw_if_index, vnet_buffer(p0)->sw_if_index[VLIB_RX]); @@ -3294,4 +3299,3 @@ VLIB_CLI_COMMAND (set_ip_classify_command, static) = { .function = set_ip_classify_command_fn, }; /* *INDENT-ON* */ - diff --git a/vnet/vnet/ip/udp.h b/vnet/vnet/ip/udp.h index 4de30f1d..f8ff777e 100644 --- a/vnet/vnet/ip/udp.h +++ b/vnet/vnet/ip/udp.h @@ -37,6 +37,7 @@ typedef enum { _ (67, dhcp_to_server) \ _ (68, dhcp_to_client) \ _ (500, ikev2) \ +_ (3784, bfd4) \ _ (4341, lisp_gpe) \ _ (4342, lisp_cp) \ _ (4739, ipfix) \ @@ -49,6 +50,7 @@ _ (6633, vpath_3) #define foreach_udp6_dst_port \ _ (547, dhcpv6_to_server) \ _ (546, dhcpv6_to_client) \ +_ (3784, bfd6) \ _ (4341, lisp_gpe6) \ _ (4342, lisp_cp6) \ _ (4790, vxlan6_gpe) \ diff --git a/vpp/vpp-api/api.c b/vpp/vpp-api/api.c index b7753092..a5ac060a 100644 --- a/vpp/vpp-api/api.c +++ b/vpp/vpp-api/api.c @@ -117,6 +117,8 @@ #include #include +#include +#include #include #include #include @@ -302,7 +304,12 @@ _(PUNT, punt) \ _(FLOW_CLASSIFY_SET_INTERFACE, flow_classify_set_interface) \ _(FLOW_CLASSIFY_DUMP, flow_classify_dump) \ _(IPSEC_SPD_DUMP, ipsec_spd_dump) \ -_(FEATURE_ENABLE_DISABLE, feature_enable_disable) +_(FEATURE_ENABLE_DISABLE, feature_enable_disable) \ +_(BFD_UDP_ADD, bfd_udp_add) \ +_(BFD_UDP_DEL, bfd_udp_del) \ +_(BFD_UDP_SESSION_DUMP, bfd_udp_session_dump) \ +_(BFD_SESSION_SET_FLAGS, bfd_session_set_flags) \ +_(WANT_BFD_EVENTS, want_bfd_events) #define QUOTE_(x) #x #define QUOTE(x) QUOTE_(x) @@ -343,6 +350,7 @@ vl_api_memclnt_delete_callback (u32 client_index) } pub_sub_handler (oam_events, OAM_EVENTS); +pub_sub_handler (bfd_events, BFD_EVENTS); #define RESOLUTION_EVENT 1 #define RESOLUTION_PENDING_EVENT 2 @@ -6766,6 +6774,163 @@ static void BAD_SW_IF_INDEX_LABEL; REPLY_MACRO (VL_API_L2_INTERFACE_PBB_TAG_REWRITE_REPLY); + +} + +static void +vl_api_bfd_udp_add_t_handler (vl_api_bfd_udp_add_t * mp) +{ + vl_api_bfd_udp_add_reply_t *rmp; + int rv; + + VALIDATE_SW_IF_INDEX (mp); + + ip46_address_t local_addr; + memset (&local_addr, 0, sizeof (local_addr)); + ip46_address_t peer_addr; + memset (&peer_addr, 0, sizeof (peer_addr)); + if (mp->is_ipv6) + { + clib_memcpy (&local_addr.ip6, mp->local_addr, sizeof (local_addr.ip6)); + clib_memcpy (&peer_addr.ip6, mp->peer_addr, sizeof (peer_addr.ip6)); + } + else + { + clib_memcpy (&local_addr.ip4, mp->local_addr, sizeof (local_addr.ip4)); + clib_memcpy (&peer_addr.ip4, mp->peer_addr, sizeof (peer_addr.ip4)); + } + + rv = bfd_udp_add_session (clib_net_to_host_u32 (mp->sw_if_index), + clib_net_to_host_u32 (mp->desired_min_tx), + clib_net_to_host_u32 (mp->required_min_rx), + mp->detect_mult, &local_addr, &peer_addr); + + BAD_SW_IF_INDEX_LABEL; + REPLY_MACRO (VL_API_BFD_UDP_ADD_REPLY); +} + +static void +vl_api_bfd_udp_del_t_handler (vl_api_bfd_udp_del_t * mp) +{ + vl_api_bfd_udp_del_reply_t *rmp; + int rv; + + VALIDATE_SW_IF_INDEX (mp); + + ip46_address_t local_addr; + memset (&local_addr, 0, sizeof (local_addr)); + ip46_address_t peer_addr; + memset (&peer_addr, 0, sizeof (peer_addr)); + if (mp->is_ipv6) + { + clib_memcpy (&local_addr.ip6, mp->local_addr, sizeof (local_addr.ip6)); + clib_memcpy (&peer_addr.ip6, mp->peer_addr, sizeof (peer_addr.ip6)); + } + else + { + clib_memcpy (&local_addr.ip4, mp->local_addr, sizeof (local_addr.ip4)); + clib_memcpy (&peer_addr.ip4, mp->peer_addr, sizeof (peer_addr.ip4)); + } + + rv = + bfd_udp_del_session (clib_net_to_host_u32 (mp->sw_if_index), &local_addr, + &peer_addr); + + BAD_SW_IF_INDEX_LABEL; + REPLY_MACRO (VL_API_BFD_UDP_DEL_REPLY); +} + +void +send_bfd_udp_session_details (unix_shared_memory_queue_t * q, u32 context, + bfd_session_t * bs) +{ + if (bs->transport != BFD_TRANSPORT_UDP4 && + bs->transport != BFD_TRANSPORT_UDP6) + { + return; + } + + vl_api_bfd_udp_session_details_t *mp = vl_msg_api_alloc (sizeof (*mp)); + memset (mp, 0, sizeof (*mp)); + mp->_vl_msg_id = ntohs (VL_API_BFD_UDP_SESSION_DETAILS); + mp->context = context; + mp->bs_index = clib_host_to_net_u32 (bs->bs_idx); + mp->state = bs->local_state; + bfd_udp_session_t *bus = &bs->udp; + bfd_udp_key_t *key = &bus->key; + mp->sw_if_index = clib_host_to_net_u32 (key->sw_if_index); + mp->is_ipv6 = !(ip46_address_is_ip4 (&key->local_addr)); + if (mp->is_ipv6) + { + clib_memcpy (mp->local_addr, &key->local_addr, + sizeof (key->local_addr)); + clib_memcpy (mp->peer_addr, &key->peer_addr, sizeof (key->peer_addr)); + } + else + { + clib_memcpy (mp->local_addr, &key->local_addr.ip4.data, + sizeof (&key->local_addr.ip4.data)); + clib_memcpy (mp->peer_addr, &key->peer_addr.ip4.data, + sizeof (&key->peer_addr.ip4.data)); + } + + vl_msg_api_send_shmem (q, (u8 *) & mp); +} + +void +bfd_event (bfd_main_t * bm, bfd_session_t * bs) +{ + vpe_api_main_t *vam = &vpe_api_main; + vpe_client_registration_t *reg; + unix_shared_memory_queue_t *q; + /* *INDENT-OFF* */ + pool_foreach (reg, vam->bfd_events_registrations, ({ + q = vl_api_client_index_to_input_queue (reg->client_index); + if (q) + { + switch (bs->transport) + { + case BFD_TRANSPORT_UDP4: + /* fallthrough */ + case BFD_TRANSPORT_UDP6: + send_bfd_udp_session_details (q, 0, bs); + } + } + })); + /* *INDENT-ON* */ +} + +static void +vl_api_bfd_udp_session_dump_t_handler (vl_api_bfd_udp_session_dump_t * mp) +{ + unix_shared_memory_queue_t *q; + + q = vl_api_client_index_to_input_queue (mp->client_index); + + if (q == 0) + return; + + bfd_session_t *bs = NULL; + /* *INDENT-OFF* */ + pool_foreach (bs, bfd_main.sessions, ({ + if (bs->transport == BFD_TRANSPORT_UDP4 || + bs->transport == BFD_TRANSPORT_UDP6) + send_bfd_udp_session_details (q, mp->context, bs); + })); + /* *INDENT-ON* */ +} + +static void +vl_api_bfd_session_set_flags_t_handler (vl_api_bfd_session_set_flags_t * mp) +{ + vl_api_bfd_session_set_flags_reply_t *rmp; + int rv; + + rv = + bfd_session_set_flags (clib_net_to_host_u32 (mp->bs_index), + mp->admin_up_down); + + REPLY_MACRO (VL_API_BFD_SESSION_SET_FLAGS_REPLY); } static void @@ -7090,6 +7255,7 @@ vpe_api_init (vlib_main_t * vm) am->to_netconf_client_registration_hash = hash_create (0, sizeof (uword)); am->from_netconf_client_registration_hash = hash_create (0, sizeof (uword)); am->oam_events_registration_hash = hash_create (0, sizeof (uword)); + am->bfd_events_registration_hash = hash_create (0, sizeof (uword)); vl_api_init (vm); vl_set_memory_region_name ("/vpe-api"); diff --git a/vpp/vpp-api/vpe.api b/vpp/vpp-api/vpe.api index 1c33f70b..b13c2606 100644 --- a/vpp/vpp-api/vpe.api +++ b/vpp/vpp-api/vpe.api @@ -4656,6 +4656,191 @@ define feature_enable_disable_reply i32 retval; }; +/** \brief Configure BFD feature + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param slow_timer - slow timer (seconds) + @param min_tx - desired min tx interval + @param min_rx - desired min rx interval + @param detect_mult - desired detection multiplier +*/ +define bfd_set_config { + u32 client_index; + u32 context; + u32 slow_timer; + u32 min_tx; + u32 min_rx; + u8 detect_mult; +}; + +/** \brief Configure BFD feature response + @param context - sender context, to match reply w/ request + @param retval - return code for the request +*/ +define bfd_set_config_reply { + u32 context; + i32 retval; +}; + +/** \brief Get BFD configuration +*/ +define bfd_get_config { + u32 client_index; + u32 context; +}; + +/** \brief Get BFD configuration response + @param context - sender context, to match reply w/ request + @param retval - return code for the request + @param slow_timer - slow timer (seconds) + @param min_tx - desired min tx interval + @param min_rx - desired min rx interval + @param detect_mult - desired detection multiplier +*/ +define bfd_get_config_reply { + u32 client_index; + u32 context; + u32 slow_timer; + u32 min_tx; + u32 min_rx; + u8 detect_mult; +}; + +/** \brief Add UDP BFD session on interface + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param sw_if_index - sw index of the interface + @param desired_min_tx - desired min transmit interval (microseconds) + @param required_min_rx - required min receive interval (microseconds) + @param detect_mult - detect multiplier (# of packets missed between connection goes down) + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 +*/ +define bfd_udp_add { + u32 client_index; + u32 context; + u32 sw_if_index; + u32 desired_min_tx; + u32 required_min_rx; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; + u8 detect_mult; +}; + +/** \brief Add UDP BFD session response + @param context - sender context, to match reply w/ request + @param retval - return code for the request + @param bs_index - index of the session created +*/ +define bfd_udp_add_reply { + u32 context; + i32 retval; + u32 bs_index; +}; + +/** \brief Delete UDP BFD session on interface + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param sw_if_index - sw index of the interface + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 +*/ +define bfd_udp_del { + u32 client_index; + u32 context; + u32 sw_if_index; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; +}; + +/** \brief Delete UDP BFD session response + @param context - sender context, to match reply w/ request + @param retval - return code for the request +*/ +define bfd_udp_del_reply { + u32 context; + i32 retval; +}; + +/** \brief Get all BFD sessions + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request +*/ +define bfd_udp_session_dump { + u32 client_index; + u32 context; +}; + +/** \brief BFD session details structure + @param context - sender context, to match reply w/ request + @param bs_index - index of the session + @param sw_if_index - sw index of the interface + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 + @param state - session state +*/ +define bfd_udp_session_details { + u32 context; + u32 bs_index; + u32 sw_if_index; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; + u8 state; +}; + +/** \brief Set flags of BFD session + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param bs_index - index of the bfd session to set flags on + @param admin_up_down - set the admin state, 1 = up, 0 = down +*/ +define bfd_session_set_flags { + u32 client_index; + u32 context; + u32 bs_index; + u8 admin_up_down; +}; + +/** \brief Reply to bfd_session_set_flags + @param context - sender context which was passed in the request + @param retval - return code of the set flags request +*/ +define bfd_session_set_flags_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Register for BFD events + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param enable_disable - 1 => register for events, 0 => cancel registration + @param pid - sender's pid +*/ +define want_bfd_events +{ + u32 client_index; + u32 context; + u32 enable_disable; + u32 pid; +}; + +/** \brief Reply for BFD events registration + @param context - returned sender context, to match reply w/ request + @param retval - return code +*/ +define want_bfd_events_reply +{ + u32 context; + i32 retval; +}; + /* * Local Variables: * eval: (c-set-style "gnu") diff --git a/vppinfra/vppinfra/mhash.c b/vppinfra/vppinfra/mhash.c index 84517e19..c917e164 100644 --- a/vppinfra/vppinfra/mhash.c +++ b/vppinfra/vppinfra/mhash.c @@ -223,7 +223,7 @@ mhash_init (mhash_t * h, uword n_value_bytes, uword n_key_bytes) } static uword -mhash_set_tmp_key (mhash_t * h, void *key) +mhash_set_tmp_key (mhash_t * h, const void *key) { u8 *key_tmp; int my_cpu = os_get_cpu_number (); @@ -251,7 +251,7 @@ mhash_set_tmp_key (mhash_t * h, void *key) } hash_pair_t * -mhash_get_pair (mhash_t * h, void *key) +mhash_get_pair (mhash_t * h, const void *key) { uword ikey; mhash_sanitize_hash_user (h); diff --git a/vppinfra/vppinfra/mhash.h b/vppinfra/vppinfra/mhash.h index 8ce8454b..102adf4e 100644 --- a/vppinfra/vppinfra/mhash.h +++ b/vppinfra/vppinfra/mhash.h @@ -101,13 +101,13 @@ mhash_key_to_mem (mhash_t * h, uword key) return vec_elt_at_index (h->key_vector_or_heap, key); } -hash_pair_t *mhash_get_pair (mhash_t * h, void *key); +hash_pair_t *mhash_get_pair (mhash_t * h, const void *key); uword mhash_set_mem (mhash_t * h, void *key, uword * new_value, uword * old_value); uword mhash_unset (mhash_t * h, void *key, uword * old_value); always_inline uword * -mhash_get (mhash_t * h, void *key) +mhash_get (mhash_t * h, const void *key) { hash_pair_t *p = mhash_get_pair (h, key); return p ? &p->value[0] : 0; -- cgit 1.2.3-korg From 9225dee9655ce607130f9bab5472441b72e25858 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Mon, 12 Dec 2016 08:36:58 +0100 Subject: make test: improve robustness and performance Introduce an API which asserts empty capture for interface. Throw exception in old API if the capture does not exist, thus making it clear if the test expects packets to arrive or not. Improve performance by not doing sleeps after starting the packet generator, rather lazily deleting captures when needed. Fix wrong usage of packet.show() in various tests. Change-Id: I456cb23316eef99b3f35f80344fe595c4db9a21c Signed-off-by: Klement Sekera --- Makefile | 4 +- test/framework.py | 41 ++++++++++++++------ test/test_bfd.py | 2 +- test/test_gre.py | 62 +++++++++++------------------- test/test_l2_fib.py | 14 ++----- test/test_l2bd_multi_instance.py | 22 +++++------ test/test_l2xc_multi_instance.py | 31 ++++++--------- test/test_lb.py | 16 +++++--- test/test_mpls.py | 29 +++----------- test/test_snat.py | 24 +++++------- test/util.py | 22 +++++++++++ test/vpp_pg_interface.py | 82 +++++++++++++++++++++++++++++----------- 12 files changed, 189 insertions(+), 160 deletions(-) (limited to 'test/util.py') diff --git a/Makefile b/Makefile index 50155664..1c7534cf 100644 --- a/Makefile +++ b/Makefile @@ -217,8 +217,6 @@ build-vpp-api: $(BR)/.bootstrap.ok VPP_PYTHON_PREFIX=$(BR)/python - - #$(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-install ,) define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install,) make -C test \ @@ -250,7 +248,7 @@ test-doc: test-wipe-doc: @make -C test wipe-doc BR=$(BR) -test-cov: +test-cov: bootstrap $(call test,vpp_lite,vpp_lite_gcov,cov) test-wipe-cov: diff --git a/test/framework.py b/test/framework.py index 2618b267..1c3e56cc 100644 --- a/test/framework.py +++ b/test/framework.py @@ -5,7 +5,6 @@ import unittest import tempfile import time import resource -from time import sleep from collections import deque from threading import Thread from inspect import getdoc @@ -181,7 +180,8 @@ class VppTestCase(unittest.TestCase): cls.logger.info("Temporary dir is %s, shm prefix is %s", cls.tempdir, cls.shm_prefix) cls.setUpConstants() - cls.pg_streams = [] + cls._captures = [] + cls._zombie_captures = [] cls.packet_infos = {} cls.verbose = 0 cls.vpp_dead = False @@ -312,17 +312,36 @@ class VppTestCase(unittest.TestCase): i.enable_capture() @classmethod - def pg_start(cls, sleep_time=1): - """ - Enable the packet-generator and send all prepared packet streams - Remove the packet streams afterwards - """ + def register_capture(cls, cap_name): + """ Register a capture in the testclass """ + # add to the list of captures with current timestamp + cls._captures.append((time.time(), cap_name)) + # filter out from zombies + cls._zombie_captures = [(stamp, name) + for (stamp, name) in cls._zombie_captures + if name != cap_name] + + @classmethod + def pg_start(cls): + """ Remove any zombie captures and enable the packet generator """ + # how long before capture is allowed to be deleted - otherwise vpp + # crashes - 100ms seems enough (this shouldn't be needed at all) + capture_ttl = 0.1 + now = time.time() + for stamp, cap_name in cls._zombie_captures: + wait = stamp + capture_ttl - now + if wait > 0: + cls.logger.debug("Waiting for %ss before deleting capture %s", + wait, cap_name) + time.sleep(wait) + now = time.time() + cls.logger.debug("Removing zombie capture %s" % cap_name) + cls.vapi.cli('packet-generator delete %s' % cap_name) + cls.vapi.cli("trace add pg-input 50") # 50 is maximum cls.vapi.cli('packet-generator enable') - sleep(sleep_time) # give VPP some time to process the packets - for stream in cls.pg_streams: - cls.vapi.cli('packet-generator delete %s' % stream) - cls.pg_streams = [] + cls._zombie_captures = cls._captures + cls._captures = [] @classmethod def create_pg_interfaces(cls, interfaces): diff --git a/test/test_bfd.py b/test/test_bfd.py index bf0e88dd..c1095d22 100644 --- a/test/test_bfd.py +++ b/test/test_bfd.py @@ -98,7 +98,7 @@ class BFDTestSession(object): p = self.create_packet() self.test.logger.debug(ppp("Sending packet:", p)) self.test.pg0.add_stream([p]) - self.test.pg_start(sleep_time=0) + self.test.pg_start() def verify_packet(self, packet): """ Verify correctness of BFD layer. """ diff --git a/test/test_gre.py b/test/test_gre.py index 0b508285..59d03e93 100644 --- a/test/test_gre.py +++ b/test/test_gre.py @@ -1,22 +1,22 @@ #!/usr/bin/env python import unittest -import socket from logging import * from framework import VppTestCase, VppTestRunner -from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint +from vpp_sub_interface import VppDot1QSubint from vpp_gre_interface import VppGreInterface from vpp_ip_route import IpRoute, RoutePath from vpp_papi_provider import L2_VTR_OP from scapy.packet import Raw -from scapy.layers.l2 import Ether, Dot1Q, ARP, GRE +from scapy.layers.l2 import Ether, Dot1Q, GRE from scapy.layers.inet import IP, UDP -from scapy.layers.inet6 import ICMPv6ND_NS, ICMPv6ND_RA, IPv6, UDP -from scapy.contrib.mpls import MPLS +from scapy.layers.inet6 import ICMPv6ND_RA, IPv6 from scapy.volatile import RandMAC, RandIP +from util import ppp, ppc + class TestGRE(VppTestCase): """ GRE Test Case """ @@ -131,7 +131,7 @@ class TestGRE(VppTestCase): def verify_filter(self, capture, sent): if not len(capture) == len(sent): - # filter out any IPv6 RAs from the captur + # filter out any IPv6 RAs from the capture for p in capture: if (p.haslayer(ICMPv6ND_RA)): capture.remove(p) @@ -163,8 +163,8 @@ class TestGRE(VppTestCase): self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) except: - rx.show() - tx.show() + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) raise def verify_tunneled_l2o4(self, src_if, capture, sent, @@ -196,8 +196,8 @@ class TestGRE(VppTestCase): self.assertEqual(rx_ip.ttl, tx_ip.ttl) except: - rx.show() - tx.show() + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) raise def verify_tunneled_vlano4(self, src_if, capture, sent, @@ -206,7 +206,7 @@ class TestGRE(VppTestCase): capture = self.verify_filter(capture, sent) self.assertEqual(len(capture), len(sent)) except: - capture.show() + ppc("Unexpected packets captured:", capture) raise for i in range(len(capture)): @@ -237,8 +237,8 @@ class TestGRE(VppTestCase): self.assertEqual(rx_ip.ttl, tx_ip.ttl) except: - rx.show() - tx.show() + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) raise def verify_decapped_4o4(self, src_if, capture, sent): @@ -261,8 +261,8 @@ class TestGRE(VppTestCase): self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) except: - rx.show() - tx.show() + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) raise def verify_decapped_6o4(self, src_if, capture, sent): @@ -284,8 +284,8 @@ class TestGRE(VppTestCase): self.assertEqual(rx_ip.hlim + 1, tx_ip.hlim) except: - rx.show() - tx.show() + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) raise def test_gre(self): @@ -333,14 +333,8 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() - - try: - self.assertEqual(0, len(rx)) - except: - error("GRE packets forwarded without DIP resolved") - error(rx.show()) - raise + self.pg0.assert_nothing_captured( + remark="GRE packets forwarded without DIP resolved") # # Add a route that resolves the tunnel's destination @@ -397,13 +391,8 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() - try: - self.assertEqual(0, len(rx)) - except: - error("GRE packets forwarded despite no SRC address match") - error(rx.show()) - raise + self.pg0.assert_nothing_captured( + remark="GRE packets forwarded despite no SRC address match") # # Configure IPv6 on the PG interface so we can route IPv6 @@ -427,13 +416,8 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() - try: - self.assertEqual(0, len(rx)) - except: - error("IPv6 GRE packets forwarded despite IPv6 not enabled on tunnel") - error(rx.show()) - raise + self.pg0.assert_nothing_captured(remark="IPv6 GRE packets forwarded " + "despite IPv6 not enabled on tunnel") # # Enable IPv6 on the tunnel diff --git a/test/test_l2_fib.py b/test/test_l2_fib.py index eb4f4e32..4855a3ea 100644 --- a/test/test_l2_fib.py +++ b/test/test_l2_fib.py @@ -68,7 +68,7 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp class TestL2fib(VppTestCase): @@ -282,8 +282,7 @@ class TestL2fib(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.pg_interfaces: remaining_packet = self.get_next_packet_info_for_interface2( @@ -327,14 +326,7 @@ class TestL2fib(VppTestCase): # Verify # Verify outgoing packet streams per packet-generator interface for i in self.pg_interfaces: - capture = i.get_capture() - self.logger.info("Verifying capture on interface %s" % i.name) - try: - self.assertEqual(len(capture), 0) - except AssertionError: - self.logger.error("The capture on interface %s is not empty!" - % i.name) - raise AssertionError("%d != 0" % len(capture)) + i.assert_nothing_captured(remark="outgoing interface") def test_l2_fib_01(self): """ L2 FIB test 1 - program 100 MAC addresses diff --git a/test/test_l2bd_multi_instance.py b/test/test_l2bd_multi_instance.py index 417df9e1..1272d765 100644 --- a/test/test_l2bd_multi_instance.py +++ b/test/test_l2bd_multi_instance.py @@ -70,7 +70,8 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp + @unittest.skip("Crashes VPP") class TestL2bdMultiInst(VppTestCase): @@ -92,12 +93,12 @@ class TestL2bdMultiInst(VppTestCase): # Packet flows mapping pg0 -> pg1, pg2 etc. cls.flows = dict() for i in range(0, len(cls.pg_interfaces), 3): - cls.flows[cls.pg_interfaces[i]] = [cls.pg_interfaces[i+1], - cls.pg_interfaces[i+2]] - cls.flows[cls.pg_interfaces[i+1]] = [cls.pg_interfaces[i], - cls.pg_interfaces[i+2]] - cls.flows[cls.pg_interfaces[i+2]] = [cls.pg_interfaces[i], - cls.pg_interfaces[i+1]] + cls.flows[cls.pg_interfaces[i]] = [cls.pg_interfaces[i + 1], + cls.pg_interfaces[i + 2]] + cls.flows[cls.pg_interfaces[i + 1]] = [cls.pg_interfaces[i], + cls.pg_interfaces[i + 2]] + cls.flows[cls.pg_interfaces[i + 2]] = [cls.pg_interfaces[i], + cls.pg_interfaces[i + 1]] # Mapping between packet-generator index and lists of test hosts cls.hosts_by_pg_idx = dict() @@ -188,7 +189,7 @@ class TestL2bdMultiInst(VppTestCase): if self.bd_deleted_list.count(bd_id) == 1: self.bd_deleted_list.remove(bd_id) for j in range(3): - pg_if = self.pg_interfaces[(i+start-1)*3+j] + pg_if = self.pg_interfaces[(i + start - 1) * 3 + j] self.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, bd_id=bd_id) self.logger.info("pg-interface %s added to bridge domain ID %d" @@ -221,7 +222,7 @@ class TestL2bdMultiInst(VppTestCase): if self.bd_deleted_list.count(bd_id) == 0: self.bd_deleted_list.append(bd_id) for j in range(3): - pg_if = self.pg_interfaces[(i+start-1)*3+j] + pg_if = self.pg_interfaces[(i + start - 1) * 3 + j] self.pg_in_bd.remove(pg_if) self.pg_not_in_bd.append(pg_if) self.logger.info("Bridge domain ID %d deleted" % bd_id) @@ -290,8 +291,7 @@ class TestL2bdMultiInst(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.pg_interfaces: remaining_packet = self.get_next_packet_info_for_interface2( diff --git a/test/test_l2xc_multi_instance.py b/test/test_l2xc_multi_instance.py index 4de76917..2e55674e 100644 --- a/test/test_l2xc_multi_instance.py +++ b/test/test_l2xc_multi_instance.py @@ -56,7 +56,7 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from framework import VppTestCase, VppTestRunner -from util import Host +from util import Host, ppp class TestL2xcMultiInst(VppTestCase): @@ -79,7 +79,7 @@ class TestL2xcMultiInst(VppTestCase): cls.flows = dict() for i in range(len(cls.pg_interfaces)): delta = 1 if i % 2 == 0 else -1 - cls.flows[cls.pg_interfaces[i]] = [cls.pg_interfaces[i+delta]] + cls.flows[cls.pg_interfaces[i]] = [cls.pg_interfaces[i + delta]] # Mapping between packet-generator index and lists of test hosts cls.hosts_by_pg_idx = dict() @@ -155,9 +155,9 @@ class TestL2xcMultiInst(VppTestCase): (Default value = 0) """ for i in range(count): - rx_if = self.pg_interfaces[i+start] + rx_if = self.pg_interfaces[i + start] delta = 1 if i % 2 == 0 else -1 - tx_if = self.pg_interfaces[i+start+delta] + tx_if = self.pg_interfaces[i + start + delta] self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index, tx_if.sw_if_index, 1) self.logger.info("Cross-connect from %s to %s created" @@ -177,9 +177,9 @@ class TestL2xcMultiInst(VppTestCase): (Default value = 0) """ for i in range(count): - rx_if = self.pg_interfaces[i+start] + rx_if = self.pg_interfaces[i + start] delta = 1 if i % 2 == 0 else -1 - tx_if = self.pg_interfaces[i+start+delta] + tx_if = self.pg_interfaces[i + start + delta] self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index, tx_if.sw_if_index, 0) self.logger.info("Cross-connect from %s to %s deleted" @@ -253,8 +253,7 @@ class TestL2xcMultiInst(VppTestCase): self.assertEqual(udp.sport, saved_packet[UDP].sport) self.assertEqual(udp.dport, saved_packet[UDP].dport) except: - self.logger.error("Unexpected or invalid packet:") - self.logger.error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet:", packet)) raise for i in self.pg_interfaces: remaining_packet = self.get_next_packet_info_for_interface2( @@ -291,21 +290,15 @@ class TestL2xcMultiInst(VppTestCase): # Verify # Verify outgoing packet streams per packet-generator interface for pg_if in self.pg_interfaces: - capture = pg_if.get_capture() if pg_if in self.pg_in_xc: - if len(capture) == 0: - raise RuntimeError("Interface %s is cross-connect sink but " - "the capture is empty!" % pg_if.name) + capture = pg_if.get_capture( + remark="interface is a cross-connect sink") self.verify_capture(pg_if, capture) elif pg_if in self.pg_not_in_xc: - try: - self.assertEqual(len(capture), 0) - except AssertionError: - raise RuntimeError("Interface %s is not cross-connect sink " - "but the capture is not empty!" - % pg_if.name) + pg_if.assert_nothing_captured( + remark="interface is not a cross-connect sink") else: - self.logger.error("Unknown interface: %s" % pg_if.name) + raise Exception("Unexpected interface: %s" % pg_if.name) def test_l2xc_inst_01(self): """ L2XC Multi-instance test 1 - create 10 cross-connects diff --git a/test/test_lb.py b/test/test_lb.py index 3e7f5e13..7037d80c 100644 --- a/test/test_lb.py +++ b/test/test_lb.py @@ -1,7 +1,7 @@ import socket from scapy.layers.inet import IP, UDP -from scapy.layers.inet6 import IPv6 +from scapy.layers.inet6 import ICMPv6ND_RA, IPv6 from scapy.layers.l2 import Ether, GRE from scapy.packet import Raw @@ -95,10 +95,16 @@ class TestLB(VppTestCase): self.assertEqual(str(inner), str(self.info.data[IPver])) def checkCapture(self, gre4, isv4): - out = self.pg0.get_capture() - # This check is edited because RA appears in output, maybe disable RA? - # self.assertEqual(len(out), 0) - self.assertLess(len(out), 20) + # RA might appear in capture + try: + out = self.pg0.get_capture() + # filter out any IPv6 RAs from the capture + for p in out: + if (p.haslayer(ICMPv6ND_RA)): + out.remove(p) + self.assertEqual(len(out), 0) + except: + pass out = self.pg1.get_capture() self.assertEqual(len(out), len(self.packets)) diff --git a/test/test_mpls.py b/test/test_mpls.py index 24fc4129..6d5eeb2b 100644 --- a/test/test_mpls.py +++ b/test/test_mpls.py @@ -11,8 +11,6 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP, ICMP from scapy.layers.inet6 import IPv6 from scapy.contrib.mpls import MPLS -from util import ppp - class TestMPLS(VppTestCase): """ MPLS Test Case """ @@ -60,7 +58,7 @@ class TestMPLS(VppTestCase): else: p = p / MPLS(label=mpls_labels[ii], ttl=mpls_ttl, s=0) if not ping: - p = (p / IP(src=src_if.local_ip4, dst=src_if.remote_ip4) / + p = (p / IP(src=src_if.local_ip4, dst=src_if.remote_ip4) / UDP(sport=1234, dport=1234) / Raw(payload)) else: @@ -331,14 +329,8 @@ class TestMPLS(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - - rx = self.pg0.get_capture() - try: - self.assertEqual(0, len(rx)) - except: - self.logger.error("MPLS non-EOS packets popped and forwarded") - self.logger.error(ppp("", rx)) - raise + self.pg0.assert_nothing_captured( + remark="MPLS non-EOS packets popped and forwarded") # # A recursive EOS x-connect, which resolves through another x-connect @@ -586,8 +578,7 @@ class TestMPLS(VppTestCase): 0, # next-hop-table-id 1, # next-hop-weight 2, # num-out-labels, - [44, 46] - ) + [44, 46]) self.vapi.sw_interface_set_flags(reply.sw_if_index, admin_up_down=1) # @@ -606,8 +597,7 @@ class TestMPLS(VppTestCase): 0, # next-hop-table-id 1, # next-hop-weight 0, # num-out-labels, - [] # out-label - ) + []) # out-label self.vapi.cli("clear trace") tx = self.create_stream_ip4(self.pg0, "10.0.0.3") @@ -632,14 +622,7 @@ class TestMPLS(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() - - try: - self.assertEqual(0, len(rx)) - except: - self.logger.error("MPLS TTL=0 packets forwarded") - self.logger.error(ppp("", rx)) - raise + self.pg0.assert_nothing_captured(remark="MPLS TTL=0 packets forwarded") # # a stream with a non-zero MPLS TTL diff --git a/test/test_snat.py b/test/test_snat.py index 5cc76f6c..fdd81f02 100644 --- a/test/test_snat.py +++ b/test/test_snat.py @@ -2,12 +2,12 @@ import socket import unittest -from logging import * from framework import VppTestCase, VppTestRunner from scapy.layers.inet import IP, TCP, UDP, ICMP from scapy.layers.l2 import Ether +from util import ppp class TestSNAT(VppTestCase): @@ -88,7 +88,7 @@ class TestSNAT(VppTestCase): :param dst_ip: Destination IP address (Default use global SNAT address) """ if dst_ip is None: - dst_ip=self.snat_addr + dst_ip = self.snat_addr pkts = [] # TCP p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / @@ -145,8 +145,8 @@ class TestSNAT(VppTestCase): self.assertNotEqual(packet[ICMP].id, self.icmp_id_in) self.icmp_id_out = packet[ICMP].id except: - error("Unexpected or invalid packet (outside network):") - error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) raise def verify_capture_in(self, capture, in_if, packet_num=3): @@ -168,8 +168,8 @@ class TestSNAT(VppTestCase): else: self.assertEqual(packet[ICMP].id, self.icmp_id_in) except: - error("Unexpected or invalid packet (inside network):") - error(packet.show()) + self.logger.error(ppp("Unexpected or invalid packet " + "(inside network):", packet)) raise def clear_snat(self): @@ -410,11 +410,10 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() - self.verify_capture_out(capture, packet_num=0) + self.pg3.assert_nothing_captured() def test_multiple_inside_interfaces(self): - """ SNAT multiple inside interfaces with non-overlapping address space """ + """SNAT multiple inside interfaces with non-overlapping address space""" self.snat_add_address(self.snat_addr) self.vapi.snat_interface_add_del_feature(self.pg0.sw_if_index) @@ -566,8 +565,7 @@ class TestSNAT(VppTestCase): self.assertEqual(tcp.dport, server_in_port) host_out_port = tcp.sport except: - error("Unexpected or invalid packet:") - error(p.show()) + self.logger.error(ppp("Unexpected or invalid packet:", p)) raise # send reply from server to host @@ -588,11 +586,9 @@ class TestSNAT(VppTestCase): self.assertEqual(tcp.sport, server_out_port) self.assertEqual(tcp.dport, host_in_port) except: - error("Unexpected or invalid packet:") - error(p.show()) + self.logger.error(ppp("Unexpected or invalid packet:"), p) raise - def tearDown(self): super(TestSNAT, self).tearDown() if not self.vpp_dead: diff --git a/test/util.py b/test/util.py index f6c6acd4..0ac23760 100644 --- a/test/util.py +++ b/test/util.py @@ -15,6 +15,28 @@ def ppp(headline, packet): return o.getvalue() +def ppc(headline, capture, limit=10): + """ Return string containing ppp() printout for a capture. + + :param headline: printed as first line of output + :param capture: packets to print + :param limit: limit the print to # of packets + """ + if not capture: + return headline + result = headline + "\n" + count = 1 + for p in capture: + result.append(ppp("Packet #%s:" % count, p)) + count += 1 + if count >= limit: + break + if limit < len(capture): + result.append( + "Capture contains %s packets in total, of which %s were printed" % + (len(capture), limit)) + + class NumericConstant(object): __metaclass__ = ABCMeta diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index 2ebcbb57..44bd1a2d 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -6,7 +6,7 @@ from vpp_interface import VppInterface from scapy.layers.l2 import Ether, ARP from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\ ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr -from util import ppp +from util import ppp, ppc class VppPGInterface(VppInterface): @@ -114,25 +114,71 @@ class VppPGInterface(VppInterface): except: pass wrpcap(self.in_path, pkts) + self.test.register_capture(self.cap_name) # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.input_cli) - self.test.pg_streams.append(self.cap_name) - self.test.vapi.cli("trace add pg-input %d" % len(pkts)) - def get_capture(self): + def get_capture(self, remark=None): """ Get captured packets :returns: iterable packets """ try: + self.wait_for_capture_file() output = rdpcap(self.out_path) except IOError: # TODO - self.test.logger.error("File %s does not exist, probably because no" + self.test.logger.debug("File %s does not exist, probably because no" " packets arrived" % self.out_path) - return [] + if remark: + raise Exception("No packets captured on %s(%s)" % + (self.name, remark)) + else: + raise Exception("No packets captured on %s" % self.name) return output + def assert_nothing_captured(self, remark=None): + if os.path.isfile(self.out_path): + try: + capture = self.get_capture() + self.test.logger.error( + ppc("Unexpected packets captured:", capture)) + except: + pass + if remark: + raise AssertionError( + "Capture file present for interface %s(%s)" % + (self.name, remark)) + else: + raise AssertionError("Capture file present for interface %s" % + self.name) + + def wait_for_capture_file(self, timeout=1): + """ + Wait until pcap capture file appears + + :param timeout: How long to wait for the packet (default 1s) + + :raises Exception: if the capture file does not appear within timeout + """ + limit = time.time() + timeout + if not os.path.isfile(self.out_path): + self.test.logger.debug( + "Waiting for capture file to appear, timeout is %ss", timeout) + else: + self.test.logger.debug("Capture file already exists") + return + while time.time() < limit: + if os.path.isfile(self.out_path): + break + time.sleep(0) # yield + if os.path.isfile(self.out_path): + self.test.logger.debug("Capture file appeared after %fs" % + (time.time() - (limit - timeout))) + else: + self.test.logger.debug("Timeout - capture file still nowhere") + raise Exception("Capture file did not appear within timeout") + def wait_for_packet(self, timeout): """ Wait for next packet captured with a timeout @@ -144,18 +190,8 @@ class VppPGInterface(VppInterface): """ limit = time.time() + timeout if self._pcap_reader is None: - self.test.logger.debug("Waiting for the capture file to appear") - while time.time() < limit: - if os.path.isfile(self.out_path): - break - time.sleep(0) # yield - if os.path.isfile(self.out_path): - self.test.logger.debug("Capture file appeared after %fs" % - (time.time() - (limit - timeout))) - self._pcap_reader = PcapReader(self.out_path) - else: - self.test.logger.debug("Timeout - capture file still nowhere") - raise Exception("Packet didn't arrive within timeout") + self.wait_for_capture_file(timeout) + self._pcap_reader = PcapReader(self.out_path) self.test.logger.debug("Waiting for packet") while time.time() < limit: @@ -197,11 +233,11 @@ class VppPGInterface(VppInterface): pg_interface.enable_capture() self.test.pg_start() self.test.logger.info(self.test.vapi.cli("show trace")) - arp_reply = pg_interface.get_capture() - if arp_reply is None or len(arp_reply) == 0: - self.test.logger.info( - "No ARP received on port %s" % - pg_interface.name) + try: + arp_reply = pg_interface.get_capture() + except: + self.test.logger.info("No ARP received on port %s" % + pg_interface.name) return arp_reply = arp_reply[0] # Make Dot1AD packet content recognizable to scapy -- cgit 1.2.3-korg From dab231a11ec96e829b22ff80c612333edc5a93e6 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Wed, 21 Dec 2016 08:50:14 +0100 Subject: make test: improve handling of packet captures Perform accounting of expected packets based on created packet infos. Use this accounting info to automatically expect (and verify) the correct number of packets to be captured. Automatically retry the read of the capture file if scapy raises an exception while doing so to handle rare cases when capture file is read while only partially written during busy wait. Don't fail assert_nothing_captured if only junk packets arrived. Change-Id: I16ec2e9410ef510d313ec16b7e13c57d0b2a63f5 Signed-off-by: Klement Sekera --- test/framework.py | 53 ++++++----- test/template_bd.py | 10 +-- test/test_bfd.py | 8 +- test/test_classifier.py | 85 ++++++++++-------- test/test_gre.py | 33 +++---- test/test_ip4.py | 19 ++-- test/test_ip4_irb.py | 17 ++-- test/test_ip6.py | 3 +- test/test_l2_fib.py | 10 +-- test/test_l2bd.py | 5 +- test/test_l2bd_multi_instance.py | 15 +--- test/test_l2xc.py | 8 +- test/test_l2xc_multi_instance.py | 5 +- test/test_lb.py | 11 ++- test/test_mpls.py | 41 +++++---- test/test_snat.py | 55 ++++++------ test/test_span.py | 11 +-- test/util.py | 17 ++-- test/vpp_pg_interface.py | 186 +++++++++++++++++++++++++++------------ 19 files changed, 321 insertions(+), 271 deletions(-) (limited to 'test/util.py') diff --git a/test/framework.py b/test/framework.py index 1b745ff3..324a64ce 100644 --- a/test/framework.py +++ b/test/framework.py @@ -10,6 +10,7 @@ from threading import Thread from inspect import getdoc from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface +from vpp_sub_interface import VppSubInterface from vpp_lo_interface import VppLoInterface from vpp_papi_provider import VppPapiProvider from scapy.packet import Raw @@ -63,9 +64,13 @@ class VppTestCase(unittest.TestCase): """List of packet infos""" return self._packet_infos - @packet_infos.setter - def packet_infos(self, value): - self._packet_infos = value + @classmethod + def get_packet_count_for_if_idx(cls, dst_if_index): + """Get the number of packet info for specified destination if index""" + if dst_if_index in cls._packet_count_for_dst_if_idx: + return cls._packet_count_for_dst_if_idx[dst_if_index] + else: + return 0 @classmethod def instance(cls): @@ -184,9 +189,9 @@ class VppTestCase(unittest.TestCase): cls.logger.info("Temporary dir is %s, shm prefix is %s", cls.tempdir, cls.shm_prefix) cls.setUpConstants() + cls.reset_packet_infos() cls._captures = [] cls._zombie_captures = [] - cls.packet_infos = {} cls.verbose = 0 cls.vpp_dead = False print(double_line_delim) @@ -394,31 +399,37 @@ class VppTestCase(unittest.TestCase): if extend > 0: packet[Raw].load += ' ' * extend - def add_packet_info_to_list(self, info): - """ - Add packet info to the testcase's packet info list - - :param info: packet info - - """ - info.index = len(self.packet_infos) - self.packet_infos[info.index] = info + @classmethod + def reset_packet_infos(cls): + """ Reset the list of packet info objects and packet counts to zero """ + cls._packet_infos = {} + cls._packet_count_for_dst_if_idx = {} - def create_packet_info(self, src_pg_index, dst_pg_index): + @classmethod + def create_packet_info(cls, src_if, dst_if): """ Create packet info object containing the source and destination indexes and add it to the testcase's packet info list - :param src_pg_index: source packet-generator index - :param dst_pg_index: destination packet-generator index + :param VppInterface src_if: source interface + :param VppInterface dst_if: destination interface :returns: _PacketInfo object """ info = _PacketInfo() - self.add_packet_info_to_list(info) - info.src = src_pg_index - info.dst = dst_pg_index + info.index = len(cls._packet_infos) + info.src = src_if.sw_if_index + info.dst = dst_if.sw_if_index + if isinstance(dst_if, VppSubInterface): + dst_idx = dst_if.parent.sw_if_index + else: + dst_idx = dst_if.sw_if_index + if dst_idx in cls._packet_count_for_dst_if_idx: + cls._packet_count_for_dst_if_idx[dst_idx] += 1 + else: + cls._packet_count_for_dst_if_idx[dst_idx] = 1 + cls._packet_infos[info.index] = info return info @staticmethod @@ -462,10 +473,10 @@ class VppTestCase(unittest.TestCase): next_index = 0 else: next_index = info.index + 1 - if next_index == len(self.packet_infos): + if next_index == len(self._packet_infos): return None else: - return self.packet_infos[next_index] + return self._packet_infos[next_index] def get_next_packet_info_for_interface(self, src_index, info): """ diff --git a/test/template_bd.py b/test/template_bd.py index 01e8b855..d70648b4 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -56,10 +56,7 @@ class BridgeDomain(object): self.pg_start() # Pick first received frame and check if it's the non-encapsulated frame - out = self.pg1.get_capture() - self.assertEqual(len(out), 1, - 'Invalid number of packets on ' - 'output: {}'.format(len(out))) + out = self.pg1.get_capture(1) pkt = out[0] # TODO: add error messages @@ -83,10 +80,7 @@ class BridgeDomain(object): self.pg_start() # Pick first received frame and check if it's corectly encapsulated. - out = self.pg0.get_capture() - self.assertEqual(len(out), 1, - 'Invalid number of packets on ' - 'output: {}'.format(len(out))) + out = self.pg0.get_capture(1) pkt = out[0] self.check_encapsulation(pkt) diff --git a/test/test_bfd.py b/test/test_bfd.py index 87a5ea4b..1ea69f55 100644 --- a/test/test_bfd.py +++ b/test/test_bfd.py @@ -184,10 +184,10 @@ class BFDTestCase(VppTestCase): self.pg_enable_capture([self.pg0]) expected_packets = 3 self.logger.info("BFD: Waiting for %d BFD packets" % expected_packets) - self.wait_for_bfd_packet() + self.wait_for_bfd_packet(2) for i in range(expected_packets): before = time.time() - self.wait_for_bfd_packet() + self.wait_for_bfd_packet(2) after = time.time() # spec says the range should be <0.75, 1>, allow extra 0.05 margin # to work around timing issues @@ -198,7 +198,7 @@ class BFDTestCase(VppTestCase): def test_zero_remote_min_rx(self): """ no packets when zero BFD RemoteMinRxInterval """ self.pg_enable_capture([self.pg0]) - p = self.wait_for_bfd_packet() + p = self.wait_for_bfd_packet(2) self.test_session.update(my_discriminator=randint(0, 40000000), your_discriminator=p[BFD].my_discriminator, state=BFDState.init, @@ -216,7 +216,7 @@ class BFDTestCase(VppTestCase): def bfd_session_up(self): self.pg_enable_capture([self.pg0]) self.logger.info("BFD: Waiting for slow hello") - p = self.wait_for_bfd_packet() + p = self.wait_for_bfd_packet(2) self.logger.info("BFD: Sending Init") self.test_session.update(my_discriminator=randint(0, 40000000), your_discriminator=p[BFD].my_discriminator, diff --git a/test/test_classifier.py b/test/test_classifier.py index 0923387c..302430f8 100644 --- a/test/test_classifier.py +++ b/test/test_classifier.py @@ -11,6 +11,7 @@ from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from util import ppp + class TestClassifier(VppTestCase): """ Classifier Test Case """ @@ -84,8 +85,7 @@ class TestClassifier(VppTestCase): """ pkts = [] for size in packet_sizes: - info = self.create_packet_info(src_if.sw_if_index, - dst_if.sw_if_index) + info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / @@ -150,8 +150,8 @@ class TestClassifier(VppTestCase): :param str dst_port: destination port number <0-ffff> """ - return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format(proto, src_ip, - dst_ip, src_port, dst_port)).rstrip('0') + return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format( + proto, src_ip, dst_ip, src_port, dst_port)).rstrip('0') @staticmethod def build_ip_match(proto='', src_ip='', dst_ip='', @@ -164,11 +164,13 @@ class TestClassifier(VppTestCase): :param str src_port: source port number <0-ffff> :param str dst_port: destination port number <0-ffff> """ - if src_ip: src_ip = socket.inet_aton(src_ip).encode('hex') - if dst_ip: dst_ip = socket.inet_aton(dst_ip).encode('hex') + if src_ip: + src_ip = socket.inet_aton(src_ip).encode('hex') + if dst_ip: + dst_ip = socket.inet_aton(dst_ip).encode('hex') - return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format(proto, src_ip, - dst_ip, src_port, dst_port)).rstrip('0') + return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format( + proto, src_ip, dst_ip, src_port, dst_port)).rstrip('0') @staticmethod def build_mac_mask(dst_mac='', src_mac='', ether_type=''): @@ -180,7 +182,7 @@ class TestClassifier(VppTestCase): """ return ('{:0>12}{:0>12}{:0>4}'.format(dst_mac, src_mac, - ether_type)).rstrip('0') + ether_type)).rstrip('0') @staticmethod def build_mac_match(dst_mac='', src_mac='', ether_type=''): @@ -190,11 +192,13 @@ class TestClassifier(VppTestCase): :param str src_mac: destination MAC address :param str ether_type: ethernet type <0-ffff> """ - if dst_mac: dst_mac = dst_mac.replace(':', '') - if src_mac: src_mac = src_mac.replace(':', '') + if dst_mac: + dst_mac = dst_mac.replace(':', '') + if src_mac: + src_mac = src_mac.replace(':', '') return ('{:0>12}{:0>12}{:0>4}'.format(dst_mac, src_mac, - ether_type)).rstrip('0') + ether_type)).rstrip('0') def create_classify_table(self, key, mask, data_offset=0, is_add=1): """Create Classify Table @@ -206,12 +210,12 @@ class TestClassifier(VppTestCase): - create(1) or delete(0) """ r = self.vapi.classify_add_del_table( - is_add, - binascii.unhexlify(mask), - match_n_vectors=(len(mask)-1)//32 + 1, - miss_next_index=0, - current_data_flag=1, - current_data_offset=data_offset) + is_add, + binascii.unhexlify(mask), + match_n_vectors=(len(mask) - 1) // 32 + 1, + miss_next_index=0, + current_data_flag=1, + current_data_offset=data_offset) self.assertIsNotNone(r, msg='No response msg for add_del_table') self.acl_tbl_idx[key] = r.new_table_index @@ -228,12 +232,12 @@ class TestClassifier(VppTestCase): - create(1) or delete(0) """ r = self.vapi.classify_add_del_session( - is_add, - table_index, - binascii.unhexlify(match), - opaque_index=0, - action=pbr_option, - metadata=vrfid) + is_add, + table_index, + binascii.unhexlify(match), + opaque_index=0, + action=pbr_option, + metadata=vrfid) self.assertIsNotNone(r, msg='No response msg for add_del_session') def input_acl_set_interface(self, intf, table_index, is_add=1): @@ -245,9 +249,9 @@ class TestClassifier(VppTestCase): - enable(1) or disable(0) """ r = self.vapi.input_acl_set_interface( - is_add, - intf.sw_if_index, - ip4_table_index=table_index) + is_add, + intf.sw_if_index, + ip4_table_index=table_index) self.assertIsNotNone(r, msg='No response msg for acl_set_interface') def test_acl_ip(self): @@ -264,14 +268,15 @@ class TestClassifier(VppTestCase): self.pg0.add_stream(pkts) self.create_classify_table('ip', self.build_ip_mask(src_ip='ffffffff')) - self.create_classify_session(self.pg0, self.acl_tbl_idx.get('ip'), - self.build_ip_match(src_ip=self.pg0.remote_ip4)) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('ip'), + self.build_ip_match(src_ip=self.pg0.remote_ip4)) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('ip')) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg1.get_capture() + pkts = self.pg1.get_capture(len(pkts)) self.verify_capture(self.pg1, pkts) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('ip'), 0) self.pg0.assert_nothing_captured(remark="packets forwarded") @@ -291,16 +296,17 @@ class TestClassifier(VppTestCase): pkts = self.create_stream(self.pg0, self.pg2, self.pg_if_packet_sizes) self.pg0.add_stream(pkts) - self.create_classify_table('mac', - self.build_mac_mask(src_mac='ffffffffffff'), data_offset=-14) - self.create_classify_session(self.pg0, self.acl_tbl_idx.get('mac'), - self.build_mac_match(src_mac=self.pg0.remote_mac)) + self.create_classify_table( + 'mac', self.build_mac_mask(src_mac='ffffffffffff'), data_offset=-14) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('mac'), + self.build_mac_match(src_mac=self.pg0.remote_mac)) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('mac')) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg2.get_capture() + pkts = self.pg2.get_capture(len(pkts)) self.verify_capture(self.pg2, pkts) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('mac'), 0) self.pg0.assert_nothing_captured(remark="packets forwarded") @@ -322,16 +328,17 @@ class TestClassifier(VppTestCase): self.create_classify_table('pbr', self.build_ip_mask(src_ip='ffffffff')) pbr_option = 1 - self.create_classify_session(self.pg0, self.acl_tbl_idx.get('pbr'), - self.build_ip_match(src_ip=self.pg0.remote_ip4), - pbr_option, self.pbr_vrfid) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('pbr'), + self.build_ip_match(src_ip=self.pg0.remote_ip4), + pbr_option, self.pbr_vrfid) self.config_pbr_fib_entry(self.pg3) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('pbr')) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg3.get_capture() + pkts = self.pg3.get_capture(len(pkts)) self.verify_capture(self.pg3, pkts) self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('pbr'), 0) self.pg0.assert_nothing_captured(remark="packets forwarded") diff --git a/test/test_gre.py b/test/test_gre.py index f00e4467..b1313044 100644 --- a/test/test_gre.py +++ b/test/test_gre.py @@ -43,8 +43,7 @@ class TestGRE(VppTestCase): def create_stream_ip4(self, src_if, src_ip, dst_ip): pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_ip, dst=dst_ip) / @@ -59,8 +58,7 @@ class TestGRE(VppTestCase): src_ip, dst_ip): pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=tunnel_src, dst=tunnel_dst) / @@ -77,8 +75,7 @@ class TestGRE(VppTestCase): src_ip, dst_ip): pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=tunnel_src, dst=tunnel_dst) / @@ -94,8 +91,7 @@ class TestGRE(VppTestCase): tunnel_src, tunnel_dst): pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=tunnel_src, dst=tunnel_dst) / @@ -113,8 +109,7 @@ class TestGRE(VppTestCase): tunnel_src, tunnel_dst, vlan): pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=tunnel_src, dst=tunnel_dst) / @@ -342,7 +337,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_tunneled_4o4(self.pg0, rx, tx, self.pg0.local_ip4, "1.1.1.2") @@ -361,7 +356,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_decapped_4o4(self.pg0, rx, tx) # @@ -426,7 +421,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_decapped_6o4(self.pg0, rx, tx) # @@ -483,7 +478,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg1.get_capture() + rx = self.pg1.get_capture(len(tx)) self.verify_tunneled_4o4(self.pg1, rx, tx, self.pg1.local_ip4, "2.2.2.2") @@ -503,7 +498,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_decapped_4o4(self.pg0, rx, tx) # @@ -564,7 +559,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_tunneled_l2o4(self.pg0, rx, tx, self.pg0.local_ip4, "2.2.2.3") @@ -578,7 +573,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_tunneled_l2o4(self.pg0, rx, tx, self.pg0.local_ip4, "2.2.2.2") @@ -635,7 +630,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_tunneled_vlano4(self.pg0, rx, tx, self.pg0.local_ip4, "2.2.2.3", @@ -651,7 +646,7 @@ class TestGRE(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg0.get_capture() + rx = self.pg0.get_capture(len(tx)) self.verify_tunneled_vlano4(self.pg0, rx, tx, self.pg0.local_ip4, "2.2.2.2", diff --git a/test/test_ip4.py b/test/test_ip4.py index f67c3b9c..df93533d 100644 --- a/test/test_ip4.py +++ b/test/test_ip4.py @@ -108,8 +108,7 @@ class TestIPv4(VppTestCase): pkts = [] for i in range(0, 257): dst_if = self.flows[src_if][i % 2] - info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / @@ -254,15 +253,13 @@ class TestIPv4FibCrud(VppTestCase): for _ in range(count): dst_addr = random.choice(dst_ips) - info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_if.remote_ip4, dst=dst_addr) / UDP(sport=1234, dport=1234) / Raw(payload)) info.data = p.copy() - size = random.choice(self.pg_if_packet_sizes) self.extend_packet(p, random.choice(self.pg_if_packet_sizes)) pkts.append(p) @@ -270,7 +267,8 @@ class TestIPv4FibCrud(VppTestCase): def _find_ip_match(self, find_in, pkt): for p in find_in: - if self.payload_to_info(str(p[Raw])) == self.payload_to_info(str(pkt[Raw])): + if self.payload_to_info(str(p[Raw])) == \ + self.payload_to_info(str(pkt[Raw])): if p[IP].src != pkt[IP].src: break if p[IP].dst != pkt[IP].dst: @@ -357,7 +355,7 @@ class TestIPv4FibCrud(VppTestCase): def setUp(self): super(TestIPv4FibCrud, self).setUp() - self.packet_infos = {} + self.reset_packet_infos() def test_1_add_routes(self): """ Add 1k routes @@ -381,10 +379,9 @@ class TestIPv4FibCrud(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg0.get_capture() + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) - def test_2_del_routes(self): """ Delete 100 routes @@ -411,7 +408,7 @@ class TestIPv4FibCrud(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg0.get_capture() + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) def test_3_add_new_routes(self): @@ -446,7 +443,7 @@ class TestIPv4FibCrud(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - pkts = self.pg0.get_capture() + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) def test_4_del_routes(self): diff --git a/test/test_ip4_irb.py b/test/test_ip4_irb.py index cf2bb150..bbec7ca7 100644 --- a/test/test_ip4_irb.py +++ b/test/test_ip4_irb.py @@ -103,8 +103,7 @@ class TestIpIrb(VppTestCase): pkts = [] for i in range(0, 257): remote_dst_host = choice(dst_ip_if.remote_hosts) - info = self.create_packet_info( - src_ip_if.sw_if_index, dst_ip_if.sw_if_index) + info = self.create_packet_info(src_ip_if, dst_ip_if) payload = self.info_to_payload(info) p = (Ether(dst=src_ip_if.local_mac, src=src_ip_if.remote_mac) / IP(src=src_ip_if.remote_ip4, @@ -121,14 +120,13 @@ class TestIpIrb(VppTestCase): packet_sizes): pkts = [] for i in range(0, 257): - info = self.create_packet_info( - src_ip_if.sw_if_index, dst_ip_if.sw_if_index) + info = self.create_packet_info(src_ip_if, dst_ip_if) payload = self.info_to_payload(info) host = choice(src_l2_if.remote_hosts) p = (Ether(src=host.mac, - dst = src_ip_if.local_mac) / + dst=src_ip_if.local_mac) / IP(src=host.ip4, dst=dst_ip_if.remote_ip4) / UDP(sport=1234, dport=1234) / @@ -152,7 +150,6 @@ class TestIpIrb(VppTestCase): ip = packet[IP] udp = packet[IP][UDP] payload_info = self.payload_to_info(str(packet[IP][UDP][Raw])) - packet_index = payload_info.index self.assertEqual(payload_info.dst, dst_ip_sw_if_index) @@ -231,8 +228,10 @@ class TestIpIrb(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rcvd1 = self.pg0.get_capture() - rcvd2 = self.pg1.get_capture() + packet_count = self.get_packet_count_for_if_idx(self.loop0.sw_if_index) + + rcvd1 = self.pg0.get_capture(packet_count) + rcvd2 = self.pg1.get_capture(packet_count) self.verify_capture(self.loop0, self.pg2, rcvd1) self.verify_capture(self.loop0, self.pg2, rcvd2) @@ -259,8 +258,6 @@ class TestIpIrb(VppTestCase): rcvd = self.pg2.get_capture() self.verify_capture_l2_to_ip(self.pg2, self.loop0, rcvd) - self.assertEqual(len(stream1) + len(stream2), len(rcvd.res)) - if __name__ == '__main__': unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip6.py b/test/test_ip6.py index 06b15f94..e8b12f68 100644 --- a/test/test_ip6.py +++ b/test/test_ip6.py @@ -116,8 +116,7 @@ class TestIPv6(VppTestCase): pkts = [] for i in range(0, 257): dst_if = self.flows[src_if][i % 2] - info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6) / diff --git a/test/test_l2_fib.py b/test/test_l2_fib.py index 4855a3ea..d4ef3f4a 100644 --- a/test/test_l2_fib.py +++ b/test/test_l2_fib.py @@ -130,11 +130,8 @@ class TestL2fib(VppTestCase): raise def setUp(self): - """ - Clear trace and packet infos before running each test. - """ super(TestL2fib, self).setUp() - self.packet_infos = {} + self.reset_packet_infos() def tearDown(self): """ @@ -236,8 +233,7 @@ class TestL2fib(VppTestCase): for i in range(0, n_int): dst_host = dst_hosts[i] src_host = random.choice(src_hosts) - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=dst_host.mac, src=src_host.mac) / IP(src=src_host.ip4, dst=dst_host.ip4) / @@ -314,7 +310,7 @@ class TestL2fib(VppTestCase): # Test # Create incoming packet streams for packet-generator interfaces for # deleted MAC addresses - self.packet_infos = {} + self.reset_packet_infos() for i in self.pg_interfaces: pkts = self.create_stream(i, self.pg_if_packet_sizes, deleted=True) i.add_stream(pkts) diff --git a/test/test_l2bd.py b/test/test_l2bd.py index 50720e64..30708a46 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -99,7 +99,7 @@ class TestL2bd(VppTestCase): Clear trace and packet infos before running each test. """ super(TestL2bd, self).setUp() - self.packet_infos = {} + self.reset_packet_infos() def tearDown(self): """ @@ -158,8 +158,7 @@ class TestL2bd(VppTestCase): dst_if = self.flows[src_if][i % 2] dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=dst_host.mac, src=src_host.mac) / IP(src=src_host.ip4, dst=dst_host.ip4) / diff --git a/test/test_l2bd_multi_instance.py b/test/test_l2bd_multi_instance.py index 1272d765..a1226222 100644 --- a/test/test_l2bd_multi_instance.py +++ b/test/test_l2bd_multi_instance.py @@ -138,7 +138,6 @@ class TestL2bdMultiInst(VppTestCase): Clear trace and packet infos before running each test. """ super(TestL2bdMultiInst, self).setUp() - self.packet_infos = {} def tearDown(self): """ @@ -243,8 +242,7 @@ class TestL2bdMultiInst(VppTestCase): for i in range(0, n_int): dst_host = dst_hosts[i] src_host = random.choice(src_hosts) - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=dst_host.mac, src=src_host.mac) / IP(src=src_host.ip4, dst=dst_host.ip4) / @@ -393,17 +391,8 @@ class TestL2bdMultiInst(VppTestCase): for pg_if in self.pg_interfaces: capture = pg_if.get_capture() if pg_if in self.pg_in_bd: - if len(capture) == 0: - raise RuntimeError("Interface %s is in BD but the capture " - "is empty!" % pg_if.name) self.verify_capture(pg_if, capture) - elif pg_if in self.pg_not_in_bd: - try: - self.assertEqual(len(capture), 0) - except AssertionError: - raise RuntimeError("Interface %s is not in BD but " - "the capture is not empty!" % pg_if.name) - else: + elif pg_if not in self.pg_not_in_bd: self.logger.error("Unknown interface: %s" % pg_if.name) def test_l2bd_inst_01(self): diff --git a/test/test_l2xc.py b/test/test_l2xc.py index 37893042..2ec4af92 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -77,11 +77,8 @@ class TestL2xc(VppTestCase): raise def setUp(self): - """ - Clear trace and packet infos before running each test. - """ super(TestL2xc, self).setUp() - self.packet_infos = {} + self.reset_packet_infos() def tearDown(self): """ @@ -123,8 +120,7 @@ class TestL2xc(VppTestCase): dst_if = self.flows[src_if][0] dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=dst_host.mac, src=src_host.mac) / IP(src=src_host.ip4, dst=dst_host.ip4) / diff --git a/test/test_l2xc_multi_instance.py b/test/test_l2xc_multi_instance.py index 2e55674e..6c28cebb 100644 --- a/test/test_l2xc_multi_instance.py +++ b/test/test_l2xc_multi_instance.py @@ -113,7 +113,7 @@ class TestL2xcMultiInst(VppTestCase): Clear trace and packet infos before running each test. """ super(TestL2xcMultiInst, self).setUp() - self.packet_infos = {} + self.reset_packet_infos() def tearDown(self): """ @@ -205,8 +205,7 @@ class TestL2xcMultiInst(VppTestCase): for i in range(0, n_int): dst_host = dst_hosts[i] src_host = random.choice(src_hosts) - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=dst_host.mac, src=src_host.mac) / IP(src=src_host.ip4, dst=dst_host.ip4) / diff --git a/test/test_lb.py b/test/test_lb.py index 9b5baaea..e5802d99 100644 --- a/test/test_lb.py +++ b/test/test_lb.py @@ -69,10 +69,10 @@ class TestLB(VppTestCase): UDP(sport=10000 + id, dport=20000 + id)) def generatePackets(self, src_if, isv4): - self.packet_infos = {} + self.reset_packet_infos() pkts = [] for pktid in self.packets: - info = self.create_packet_info(src_if.sw_if_index, pktid) + info = self.create_packet_info(src_if, self.pg1) payload = self.info_to_payload(info) ip = self.getIPv4Flow(pktid) if isv4 else self.getIPv6Flow(pktid) packet = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / @@ -90,14 +90,13 @@ class TestLB(VppTestCase): self.assertEqual(gre.version, 0) inner = IPver(str(gre.payload)) payload_info = self.payload_to_info(str(inner[Raw])) - self.info = self.get_next_packet_info_for_interface2( - self.pg0.sw_if_index, payload_info.dst, self.info) + self.info = self.packet_infos[payload_info.index] + self.assertEqual(payload_info.src, self.pg0.sw_if_index) self.assertEqual(str(inner), str(self.info.data[IPver])) def checkCapture(self, gre4, isv4): self.pg0.assert_nothing_captured() - out = self.pg1.get_capture() - self.assertEqual(len(out), len(self.packets)) + out = self.pg1.get_capture(len(self.packets)) load = [0] * len(self.ass) self.info = None diff --git a/test/test_mpls.py b/test/test_mpls.py index 6d5eeb2b..806e69dc 100644 --- a/test/test_mpls.py +++ b/test/test_mpls.py @@ -12,6 +12,7 @@ from scapy.layers.inet import IP, UDP, ICMP from scapy.layers.inet6 import IPv6 from scapy.contrib.mpls import MPLS + class TestMPLS(VppTestCase): """ MPLS Test Case """ @@ -44,11 +45,17 @@ class TestMPLS(VppTestCase): super(TestMPLS, self).tearDown() # the default of 64 matches the IP packet TTL default - def create_stream_labelled_ip4(self, src_if, mpls_labels, mpls_ttl=255, ping=0, ip_itf=None): + def create_stream_labelled_ip4( + self, + src_if, + mpls_labels, + mpls_ttl=255, + ping=0, + ip_itf=None): + self.reset_packet_infos() pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = Ether(dst=src_if.local_mac, src=src_if.remote_mac) @@ -71,10 +78,10 @@ class TestMPLS(VppTestCase): return pkts def create_stream_ip4(self, src_if, dst_ip): + self.reset_packet_infos() pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_if.remote_ip4, dst=dst_ip) / @@ -85,10 +92,10 @@ class TestMPLS(VppTestCase): return pkts def create_stream_labelled_ip6(self, src_if, mpls_label, mpls_ttl): + self.reset_packet_infos() pkts = [] for i in range(0, 257): - info = self.create_packet_info(src_if.sw_if_index, - src_if.sw_if_index) + info = self.create_packet_info(src_if, src_if) payload = self.info_to_payload(info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / MPLS(label=mpls_label, ttl=mpls_ttl) / @@ -342,7 +349,6 @@ class TestMPLS(VppTestCase): labels=[44, 45])]) route_34_eos.add_vpp_config() - self.vapi.cli("clear trace") tx = self.create_stream_labelled_ip4(self.pg0, [34]) self.pg0.add_stream(tx) @@ -628,7 +634,6 @@ class TestMPLS(VppTestCase): # a stream with a non-zero MPLS TTL # PG0 is in the default table # - self.vapi.cli("clear trace") tx = self.create_stream_labelled_ip4(self.pg0, [0]) self.pg0.add_stream(tx) @@ -692,9 +697,9 @@ class TestMPLS(VppTestCase): # A de-agg route - next-hop lookup in default table # route_34_eos = MplsRoute(self, 34, 1, - [RoutePath("0.0.0.0", - 0xffffffff, - nh_table_id=0)]) + [RoutePath("0.0.0.0", + 0xffffffff, + nh_table_id=0)]) route_34_eos.add_vpp_config() # @@ -716,9 +721,9 @@ class TestMPLS(VppTestCase): # A de-agg route - next-hop lookup in non-default table # route_35_eos = MplsRoute(self, 35, 1, - [RoutePath("0.0.0.0", - 0xffffffff, - nh_table_id=1)]) + [RoutePath("0.0.0.0", + 0xffffffff, + nh_table_id=1)]) route_35_eos.add_vpp_config() # @@ -727,13 +732,15 @@ class TestMPLS(VppTestCase): # default table and egress unlabelled in the non-default # self.vapi.cli("clear trace") - tx = self.create_stream_labelled_ip4(self.pg0, [35], ping=1, ip_itf=self.pg1) + tx = self.create_stream_labelled_ip4( + self.pg0, [35], ping=1, ip_itf=self.pg1) self.pg0.add_stream(tx) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - rx = self.pg1.get_capture() + packet_count = self.get_packet_count_for_if_idx(self.pg0.sw_if_index) + rx = self.pg1.get_capture(packet_count) self.verify_capture_ip4(self.pg1, rx, tx, ping_resp=1) route_35_eos.remove_vpp_config() diff --git a/test/test_snat.py b/test/test_snat.py index 8985c3e4..ca2e52a7 100644 --- a/test/test_snat.py +++ b/test/test_snat.py @@ -241,7 +241,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in @@ -249,7 +249,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) def test_static_in(self): @@ -270,7 +270,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_out(capture, nat_ip, True) # out2in @@ -278,7 +278,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) def test_static_out(self): @@ -299,7 +299,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) # in2out @@ -307,7 +307,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_out(capture, nat_ip, True) def test_static_with_port_in(self): @@ -333,7 +333,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in @@ -341,7 +341,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) def test_static_with_port_out(self): @@ -367,7 +367,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) # in2out @@ -375,7 +375,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_out(capture) def test_static_vrf_aware(self): @@ -401,7 +401,7 @@ class TestSNAT(VppTestCase): self.pg4.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture, nat_ip1, True) # inside interface VRF don't match SNAT static mapping VRF (packets @@ -427,7 +427,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 1st interface @@ -435,7 +435,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() + capture = self.pg0.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg0) # in2out 2nd interface @@ -443,7 +443,7 @@ class TestSNAT(VppTestCase): self.pg1.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 2nd interface @@ -451,7 +451,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg1.get_capture() + capture = self.pg1.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg1) # in2out 3rd interface @@ -459,7 +459,7 @@ class TestSNAT(VppTestCase): self.pg2.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 3rd interface @@ -467,7 +467,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg2.get_capture() + capture = self.pg2.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg2) def test_inside_overlapping_interfaces(self): @@ -485,7 +485,7 @@ class TestSNAT(VppTestCase): self.pg4.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 1st interface @@ -493,7 +493,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg4.get_capture() + capture = self.pg4.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg4) # in2out 2nd interface @@ -501,7 +501,7 @@ class TestSNAT(VppTestCase): self.pg5.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 2nd interface @@ -509,7 +509,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg5.get_capture() + capture = self.pg5.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg5) # in2out 3rd interface @@ -517,7 +517,7 @@ class TestSNAT(VppTestCase): self.pg6.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg3.get_capture() + capture = self.pg3.get_capture(len(pkts)) self.verify_capture_out(capture) # out2in 3rd interface @@ -525,7 +525,7 @@ class TestSNAT(VppTestCase): self.pg3.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg6.get_capture() + capture = self.pg6.get_capture(len(pkts)) self.verify_capture_in(capture, self.pg6) def test_hairpinning(self): @@ -553,8 +553,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(p) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() - self.assertEqual(1, len(capture)) + capture = self.pg0.get_capture(1) p = capture[0] try: ip = p[IP] @@ -575,8 +574,7 @@ class TestSNAT(VppTestCase): self.pg0.add_stream(p) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - capture = self.pg0.get_capture() - self.assertEqual(1, len(capture)) + capture = self.pg0.get_capture(1) p = capture[0] try: ip = p[IP] @@ -613,8 +611,7 @@ class TestSNAT(VppTestCase): self.pg_start() # verify number of translated packet - capture = self.pg1.get_capture() - self.assertEqual(pkts_num, len(capture)) + self.pg1.get_capture(pkts_num) def tearDown(self): super(TestSNAT, self).tearDown() diff --git a/test/test_span.py b/test/test_span.py index e42fbd77..41507092 100644 --- a/test/test_span.py +++ b/test/test_span.py @@ -85,8 +85,7 @@ class TestSpan(VppTestCase): pkts = [] for i in range(0, TestSpan.pkts_per_burst): dst_if = self.flows[src_if][0] - pkt_info = self.create_packet_info( - src_if.sw_if_index, dst_if.sw_if_index) + pkt_info = self.create_packet_info(src_if, dst_if) payload = self.info_to_payload(pkt_info) p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / @@ -184,9 +183,11 @@ class TestSpan(VppTestCase): # Verify packets outgoing packet streams on mirrored interface (pg2) self.logger.info("Verifying capture on interfaces %s and %s" % (self.pg1.name, self.pg2.name)) - self.verify_capture(self.pg1, - self.pg1.get_capture(), - self.pg2.get_capture()) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + self.verify_capture( + self.pg1, + self.pg1.get_capture(), + self.pg2.get_capture(pg2_expected)) if __name__ == '__main__': diff --git a/test/util.py b/test/util.py index 0ac23760..6658febf 100644 --- a/test/util.py +++ b/test/util.py @@ -24,17 +24,14 @@ def ppc(headline, capture, limit=10): """ if not capture: return headline - result = headline + "\n" - count = 1 - for p in capture: - result.append(ppp("Packet #%s:" % count, p)) - count += 1 - if count >= limit: - break + tail = "" if limit < len(capture): - result.append( - "Capture contains %s packets in total, of which %s were printed" % - (len(capture), limit)) + tail = "\nPrint limit reached, %s out of %s packets printed" % ( + len(capture), limit) + limit = len(capture) + body = "".join([ppp("Packet #%s:" % count, p) + for count, p in zip(range(0, limit), capture)]) + return "%s\n%s%s" % (headline, body, tail) class NumericConstant(object): diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index 634d7d3e..f4305275 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,5 +1,6 @@ import os import time +from traceback import format_exc from scapy.utils import wrpcap, rdpcap, PcapReader from vpp_interface import VppInterface @@ -130,30 +131,20 @@ class VppPGInterface(VppInterface): # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.input_cli) - def get_capture(self, remark=None, filter_fn=is_ipv6_misc): - """ - Get captured packets - - :param remark: remark printed into debug logs - :param filter_fn: filter applied to each packet, packets for which - the filter returns True are removed from capture - :returns: iterable packets - """ + def _get_capture(self, timeout, filter_out_fn=is_ipv6_misc): + """ Helper method to get capture and filter it """ try: - self.wait_for_capture_file() + if not self.wait_for_capture_file(timeout): + return None output = rdpcap(self.out_path) self.test.logger.debug("Capture has %s packets" % len(output.res)) - except IOError: # TODO - self.test.logger.debug("File %s does not exist, probably because no" - " packets arrived" % self.out_path) - if remark: - raise Exception("No packets captured on %s(%s)" % - (self.name, remark)) - else: - raise Exception("No packets captured on %s" % self.name) + except: + self.test.logger.debug("Exception in scapy.rdpcap(%s): %s" % + (self.out_path, format_exc())) + return None before = len(output.res) - if filter_fn: - output.res = [p for p in output.res if not filter_fn(p)] + if filter_out_fn: + output.res = [p for p in output.res if not filter_out_fn(p)] removed = len(output.res) - before if removed: self.test.logger.debug( @@ -161,21 +152,75 @@ class VppPGInterface(VppInterface): (removed, len(output.res))) return output - def assert_nothing_captured(self, remark=None): + def get_capture(self, expected_count=None, remark=None, timeout=1, + filter_out_fn=is_ipv6_misc): + """ Get captured packets + + :param expected_count: expected number of packets to capture, if None, + then self.test.packet_count_for_dst_pg_idx is + used to lookup the expected count + :param remark: remark printed into debug logs + :param timeout: how long to wait for packets + :param filter_out_fn: filter applied to each packet, packets for which + the filter returns True are removed from capture + :returns: iterable packets + """ + remaining_time = timeout + capture = None + name = self.name if remark is None else "%s (%s)" % (self.name, remark) + based_on = "based on provided argument" + if expected_count is None: + expected_count = \ + self.test.get_packet_count_for_if_idx(self.sw_if_index) + based_on = "based on stored packet_infos" + self.test.logger.debug("Expecting to capture %s(%s) packets on %s" % ( + expected_count, based_on, name)) + if expected_count == 0: + raise Exception( + "Internal error, expected packet count for %s is 0!" % name) + while remaining_time > 0: + before = time.time() + capture = self._get_capture(remaining_time, filter_out_fn) + elapsed_time = time.time() - before + if capture: + if len(capture.res) == expected_count: + # bingo, got the packets we expected + return capture + remaining_time -= elapsed_time + if capture: + raise Exception("Captured packets mismatch, captured %s packets, " + "expected %s packets on %s" % + (len(capture.res), expected_count, name)) + else: + raise Exception("No packets captured on %s" % name) + + def assert_nothing_captured(self, remark=None, filter_out_fn=is_ipv6_misc): + """ Assert that nothing unfiltered was captured on interface + + :param remark: remark printed into debug logs + :param filter_out_fn: filter applied to each packet, packets for which + the filter returns True are removed from capture + """ if os.path.isfile(self.out_path): try: - capture = self.get_capture(remark=remark) + capture = self.get_capture( + 0, remark=remark, filter_out_fn=filter_out_fn) + if capture: + if len(capture.res) == 0: + # junk filtered out, we're good + return self.test.logger.error( ppc("Unexpected packets captured:", capture)) except: pass if remark: raise AssertionError( - "Capture file present for interface %s(%s)" % + "Non-empty capture file present for interface %s(%s)" % (self.name, remark)) else: - raise AssertionError("Capture file present for interface %s" % - self.name) + raise AssertionError( + "Non-empty capture file present for interface %s" % + self.name) def wait_for_capture_file(self, timeout=1): """ @@ -183,15 +228,17 @@ class VppPGInterface(VppInterface): :param timeout: How long to wait for the packet (default 1s) - :raises Exception: if the capture file does not appear within timeout + :returns: True/False if the file is present or appears within timeout """ limit = time.time() + timeout if not os.path.isfile(self.out_path): - self.test.logger.debug( - "Waiting for capture file to appear, timeout is %ss", timeout) + self.test.logger.debug("Waiting for capture file %s to appear, " + "timeout is %ss" % (self.out_path, timeout)) else: - self.test.logger.debug("Capture file already exists") - return + self.test.logger.debug( + "Capture file %s already exists" % + self.out_path) + return True while time.time() < limit: if os.path.isfile(self.out_path): break @@ -201,9 +248,10 @@ class VppPGInterface(VppInterface): (time.time() - (limit - timeout))) else: self.test.logger.debug("Timeout - capture file still nowhere") - raise Exception("Capture file did not appear within timeout") + return False + return True - def wait_for_packet(self, timeout): + def wait_for_packet(self, timeout, filter_out_fn=is_ipv6_misc): """ Wait for next packet captured with a timeout @@ -212,18 +260,34 @@ class VppPGInterface(VppInterface): :returns: Captured packet if no packet arrived within timeout :raises Exception: if no packet arrives within timeout """ - limit = time.time() + timeout + deadline = time.time() + timeout if self._pcap_reader is None: - self.wait_for_capture_file(timeout) - self._pcap_reader = PcapReader(self.out_path) + if not self.wait_for_capture_file(timeout): + raise Exception("Capture file %s did not appear within " + "timeout" % self.out_path) + while time.time() < deadline: + try: + self._pcap_reader = PcapReader(self.out_path) + break + except: + self.test.logger.debug("Exception in scapy.PcapReader(%s): " + "%s" % (self.out_path, format_exc())) + if not self._pcap_reader: + raise Exception("Capture file %s did not appear within " + "timeout" % self.out_path) self.test.logger.debug("Waiting for packet") - while time.time() < limit: + while time.time() < deadline: p = self._pcap_reader.recv() if p is not None: - self.test.logger.debug("Packet received after %fs", - (time.time() - (limit - timeout))) - return p + if filter_out_fn is not None and filter_out_fn(p): + self.test.logger.debug( + "Packet received after %ss was filtered out" % + (time.time() - (deadline - timeout))) + else: + self.test.logger.debug("Packet received after %fs" % + (time.time() - (deadline - timeout))) + return p time.sleep(0) # yield self.test.logger.debug("Timeout - no packets received") raise Exception("Packet didn't arrive within timeout") @@ -258,12 +322,12 @@ class VppPGInterface(VppInterface): self.test.pg_start() self.test.logger.info(self.test.vapi.cli("show trace")) try: - arp_reply = pg_interface.get_capture(filter_fn=None) + captured_packet = pg_interface.wait_for_packet(1) except: self.test.logger.info("No ARP received on port %s" % pg_interface.name) return - arp_reply = arp_reply[0] + arp_reply = captured_packet.copy() # keep original for exception # Make Dot1AD packet content recognizable to scapy if arp_reply.type == 0x88a8: arp_reply.type = 0x8100 @@ -274,19 +338,19 @@ class VppPGInterface(VppInterface): (self.name, arp_reply[ARP].hwsrc)) self._local_mac = arp_reply[ARP].hwsrc else: - self.test.logger.info( - "No ARP received on port %s" % - pg_interface.name) + self.test.logger.info("No ARP received on port %s" % + pg_interface.name) except: self.test.logger.error( - ppp("Unexpected response to ARP request:", arp_reply)) + ppp("Unexpected response to ARP request:", captured_packet)) raise - def resolve_ndp(self, pg_interface=None): + def resolve_ndp(self, pg_interface=None, timeout=1): """Resolve NDP using provided packet-generator interface :param pg_interface: interface used to resolve, if None then this interface is used + :param timeout: how long to wait for response before giving up """ if pg_interface is None: @@ -297,17 +361,19 @@ class VppPGInterface(VppInterface): pg_interface.add_stream(ndp_req) pg_interface.enable_capture() self.test.pg_start() - self.test.logger.info(self.test.vapi.cli("show trace")) - replies = pg_interface.get_capture(filter_fn=None) - if replies is None or len(replies) == 0: - self.test.logger.info( - "No NDP received on port %s" % - pg_interface.name) - return + now = time.time() + deadline = now + timeout # Enabling IPv6 on an interface can generate more than the # ND reply we are looking for (namely MLD). So loop through # the replies to look for want we want. - for ndp_reply in replies: + while now < deadline: + try: + captured_packet = pg_interface.wait_for_packet( + deadline - now, filter_out_fn=None) + except: + self.test.logger.error("Timeout while waiting for NDP response") + raise + ndp_reply = captured_packet.copy() # keep original for exception # Make Dot1AD packet content recognizable to scapy if ndp_reply.type == 0x88a8: ndp_reply.type = 0x8100 @@ -318,9 +384,13 @@ class VppPGInterface(VppInterface): self.test.logger.info("VPP %s MAC address is %s " % (self.name, opt.lladdr)) self._local_mac = opt.lladdr + self.test.logger.debug(self.test.vapi.cli("show trace")) + # we now have the MAC we've been after + return except: self.test.logger.info( - ppp("Unexpected response to NDP request:", ndp_reply)) - # if no packets above provided the local MAC, then this failed. - if not hasattr(self, '_local_mac'): - raise + ppp("Unexpected response to NDP request:", captured_packet)) + now = time.time() + + self.test.logger.debug(self.test.vapi.cli("show trace")) + raise Exception("Timeout while waiting for NDP response") -- cgit 1.2.3-korg From d81da8c0b5094de6635ad9575a2a2bb6f44070dc Mon Sep 17 00:00:00 2001 From: Eyal Bari Date: Wed, 11 Jan 2017 13:39:54 +0200 Subject: vxlan unit test - minor fixes moved ip4_range and ip4n_range to util added n_ucast_tunnels Change-Id: I9140c4e54a0636d90a97db03da842f5183319af5 Signed-off-by: Eyal Bari --- test/template_bd.py | 15 ++++++--------- test/test_vxlan.py | 39 +++++++++++++++++++-------------------- test/util.py | 10 ++++++++++ 3 files changed, 35 insertions(+), 29 deletions(-) (limited to 'test/util.py') diff --git a/test/template_bd.py b/test/template_bd.py index 6c384922..91d5dd71 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -5,6 +5,8 @@ from abc import abstractmethod, ABCMeta from scapy.layers.l2 import Ether, Raw from scapy.layers.inet import IP, UDP +from util import ip4_range + class BridgeDomain(object): """ Bridge domain abstraction """ @@ -110,7 +112,7 @@ class BridgeDomain(object): self.pg_start() # Get packet from each tunnel and assert it's corectly encapsulated. - out = self.pg0.get_capture(10) + out = self.pg0.get_capture(self.n_ucast_tunnels) for pkt in out: self.check_encapsulation(pkt, self.ucast_flood_bd, True) payload = self.decapsulate(pkt) @@ -135,10 +137,6 @@ class BridgeDomain(object): payload = self.decapsulate(pkt) self.assert_eq_pkts(payload, self.frame_reply) - @staticmethod - def ipn_to_ip(ipn): - return '.'.join(str(i) for i in bytearray(ipn)) - def test_mcast_rcv(self): """ Multicast receive test Send 20 encapsulated frames from pg0 only 10 match unicast tunnels @@ -148,10 +146,9 @@ class BridgeDomain(object): ip_range_start = 10 ip_range_end = 30 mcast_stream = [ - self.encap_mcast(self.frame_request, self.ipn_to_ip(ip), mac, - self.mcast_flood_bd) - for ip in self.ip4_range(self.pg0.remote_ip4n, - ip_range_start, ip_range_end)] + self.encap_mcast(self.frame_request, ip, mac, self.mcast_flood_bd) + for ip in ip4_range(self.pg0.remote_ip4, + ip_range_start, ip_range_end)] self.pg0.add_stream(mcast_stream) self.pg2.enable_capture() self.pg_start() diff --git a/test/test_vxlan.py b/test/test_vxlan.py index 845b8175..9dccaf50 100644 --- a/test/test_vxlan.py +++ b/test/test_vxlan.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import socket +from util import ip4n_range import unittest from framework import VppTestCase, VppTestRunner from template_bd import BridgeDomain @@ -34,7 +35,7 @@ class TestVxlan(BridgeDomain, VppTestCase): Encapsulate the original payload frame by adding VXLAN header with its UDP, IP and Ethernet fields """ - return (Ether(src=src_mac, dst=self.mcast_mac4) / + return (Ether(src=src_mac, dst=self.mcast_mac) / IP(src=src_ip, dst=self.mcast_ip4) / UDP(sport=self.dport, dport=self.dport, chksum=0) / VXLAN(vni=vni, flags=self.flags) / @@ -68,24 +69,19 @@ class TestVxlan(BridgeDomain, VppTestCase): # Verify VNI self.assertEqual(pkt[VXLAN].vni, vni) - @staticmethod - def ip4_range(ip4n, s=10, e=20): - base = str(bytearray(ip4n)[:3]) - return ((base + ip) for ip in str(bytearray(range(s, e)))) - @classmethod - def create_vxlan_flood_test_bd(cls, vni): + def create_vxlan_flood_test_bd(cls, vni, n_ucast_tunnels): # Create 10 ucast vxlan tunnels under bd ip_range_start = 10 - ip_range_end = 20 + ip_range_end = ip_range_start + n_ucast_tunnels next_hop_address = cls.pg0.remote_ip4n - for dest_addr in cls.ip4_range(next_hop_address, ip_range_start, - ip_range_end): - # add host route so dest_addr will not be resolved - cls.vapi.ip_add_del_route(dest_addr, 32, next_hop_address) + for dest_ip4n in ip4n_range(next_hop_address, ip_range_start, + ip_range_end): + # add host route so dest_ip4n will not be resolved + cls.vapi.ip_add_del_route(dest_ip4n, 32, next_hop_address) r = cls.vapi.vxlan_add_del_tunnel( src_addr=cls.pg0.local_ip4n, - dst_addr=dest_addr, + dst_addr=dest_ip4n, vni=vni) cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, bd_id=vni) @@ -93,12 +89,12 @@ class TestVxlan(BridgeDomain, VppTestCase): def add_del_mcast_load(cls, is_add): ip_range_start = 10 ip_range_end = 210 - for dest_addr in cls.ip4_range(cls.mcast_ip4n, ip_range_start, - ip_range_end): - vni = bytearray(dest_addr)[3] + for dest_ip4n in ip4n_range(cls.mcast_ip4n, ip_range_start, + ip_range_end): + vni = bytearray(dest_ip4n)[3] cls.vapi.vxlan_add_del_tunnel( src_addr=cls.pg0.local_ip4n, - dst_addr=dest_addr, + dst_addr=dest_ip4n, mcast_sw_if_index=1, vni=vni, is_add=is_add) @@ -139,7 +135,7 @@ class TestVxlan(BridgeDomain, VppTestCase): cls.mcast_ip4 = '239.1.1.1' cls.mcast_ip4n = socket.inet_pton(socket.AF_INET, cls.mcast_ip4) iplong = atol(cls.mcast_ip4) - cls.mcast_mac4 = "01:00:5e:%02x:%02x:%02x" % ( + cls.mcast_mac = "01:00:5e:%02x:%02x:%02x" % ( (iplong >> 16) & 0x7F, (iplong >> 8) & 0xFF, iplong & 0xFF) # Create VXLAN VTEP on VPP pg0, and put vxlan_tunnel0 and pg1 @@ -155,8 +151,10 @@ class TestVxlan(BridgeDomain, VppTestCase): bd_id=cls.single_tunnel_bd) # Setup vni 2 to test multicast flooding + cls.n_ucast_tunnels = 10 cls.mcast_flood_bd = 2 - cls.create_vxlan_flood_test_bd(cls.mcast_flood_bd) + cls.create_vxlan_flood_test_bd(cls.mcast_flood_bd, + cls.n_ucast_tunnels) r = cls.vapi.vxlan_add_del_tunnel( src_addr=cls.pg0.local_ip4n, dst_addr=cls.mcast_ip4n, @@ -173,7 +171,8 @@ class TestVxlan(BridgeDomain, VppTestCase): # Setup vni 3 to test unicast flooding cls.ucast_flood_bd = 3 - cls.create_vxlan_flood_test_bd(cls.ucast_flood_bd) + cls.create_vxlan_flood_test_bd(cls.ucast_flood_bd, + cls.n_ucast_tunnels) cls.vapi.sw_interface_set_l2_bridge(cls.pg3.sw_if_index, bd_id=cls.ucast_flood_bd) except Exception: diff --git a/test/util.py b/test/util.py index 6658febf..ae887a54 100644 --- a/test/util.py +++ b/test/util.py @@ -34,6 +34,16 @@ def ppc(headline, capture, limit=10): return "%s\n%s%s" % (headline, body, tail) +def ip4_range(ip4, s, e): + tmp = ip4.rsplit('.', 1)[0] + return ("%s.%d" % (tmp, i) for i in range(s, e)) + + +def ip4n_range(ip4n, s, e): + ip4 = socket.inet_ntop(socket.AF_INET, ip4n) + return (socket.inet_pton(socket.AF_INET, ip) for ip in ip4_range(ip4, s, e)) + + class NumericConstant(object): __metaclass__ = ABCMeta -- cgit 1.2.3-korg From 72715ee4e2bba6fd4ce5c077bca68bb08ac729a0 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 17 Jan 2017 10:37:05 +0100 Subject: make test: add checkstyle target Change-Id: I59d3c3bc77474c96e1d6fa51811c1b13fb9a6c5b Signed-off-by: Klement Sekera --- Makefile | 4 ++++ test/Makefile | 17 +++++++++++++++++ test/test_bfd.py | 2 -- test/util.py | 3 ++- 4 files changed, 23 insertions(+), 3 deletions(-) (limited to 'test/util.py') diff --git a/Makefile b/Makefile index 12f40483..f1813a30 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,7 @@ help: @echo " test-wipe-doc - wipe documentation for test framework" @echo " test-cov - generate code coverage report for test framework" @echo " test-wipe-cov - wipe code coverage report for test framework" + @echo " test-checkstyle - check PEP8 compliance for test framework" @echo "" @echo "Make Arguments:" @echo " V=[0|1] - set build verbosity level" @@ -239,6 +240,9 @@ test-cov: bootstrap test-wipe-cov: @make -C test wipe-cov +test-checkstyle: + @make -C test checkstyle + retest: $(call test,vpp_lite,vpp_lite,retest) diff --git a/test/Makefile b/test/Makefile index 543fe913..db64ad93 100644 --- a/test/Makefile +++ b/test/Makefile @@ -77,6 +77,20 @@ cov: wipe-cov reset verify-python-path $(PAPI_INSTALL_DONE) wipe-cov: wipe @rm -rf $(BUILD_COV_DIR) +.PHONY: checkstyle +checkstyle: verify-python-path + @virtualenv $(PYTHON_VENV_PATH) -p python2.7 + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) pep8" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate &&\ + pep8 --show-source -v $(WS_ROOT)/test/*.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Test framework PEP8 compliance check FAILED \" &&\ + echo \"*******************************************************************\" &&\ + false)" + @echo "*******************************************************************" + @echo "* Test framework PEP8 compliance check passed" + @echo "*******************************************************************" + help: @echo "Running tests:" @echo "" @@ -106,3 +120,6 @@ help: @echo " test-cov - generate code coverage report for test framework" @echo " test-wipe-cov - wipe code coverage report for test framework" @echo "" + @echo "Verifying code-style" + @echo " test-checkstyle - check PEP8 compliance" + @echo "" diff --git a/test/test_bfd.py b/test/test_bfd.py index b6222524..b7832247 100644 --- a/test/test_bfd.py +++ b/test/test_bfd.py @@ -27,8 +27,6 @@ class BFDAPITestCase(VppTestCase): super(BFDAPITestCase, cls).tearDownClass() raise - - def test_add_bfd(self): """ create a BFD session """ session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) diff --git a/test/util.py b/test/util.py index ae887a54..79893602 100644 --- a/test/util.py +++ b/test/util.py @@ -41,7 +41,8 @@ def ip4_range(ip4, s, e): def ip4n_range(ip4n, s, e): ip4 = socket.inet_ntop(socket.AF_INET, ip4n) - return (socket.inet_pton(socket.AF_INET, ip) for ip in ip4_range(ip4, s, e)) + return (socket.inet_pton(socket.AF_INET, ip) + for ip in ip4_range(ip4, s, e)) class NumericConstant(object): -- cgit 1.2.3-korg From 46a87adf10d41af4b1b14f06bdab33228cbaae95 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Mon, 2 Jan 2017 08:22:23 +0100 Subject: BFD: IPv6 support Change-Id: Iaa9538c7cca500c04cf2704e5bf87480543cfcdf Signed-off-by: Klement Sekera --- src/vnet/bfd/bfd_main.c | 52 +++++----- src/vnet/bfd/bfd_main.h | 2 +- src/vnet/bfd/bfd_udp.c | 245 ++++++++++++++++++++++++++++++++++++++---------- src/vnet/bfd/bfd_udp.h | 6 +- test/bfd.py | 14 ++- test/test_bfd.py | 221 +++++++++++++++++++++++++++---------------- test/util.py | 11 ++- test/vpp_interface.py | 11 ++- 8 files changed, 394 insertions(+), 168 deletions(-) (limited to 'test/util.py') diff --git a/src/vnet/bfd/bfd_main.c b/src/vnet/bfd/bfd_main.c index 62be1842..7e1a2ef2 100644 --- a/src/vnet/bfd/bfd_main.c +++ b/src/vnet/bfd/bfd_main.c @@ -34,16 +34,9 @@ bfd_us_to_clocks (bfd_main_t * bm, u64 us) static vlib_node_registration_t bfd_process_node; -typedef enum -{ -#define F(t, n) BFD_OUTPUT_##t, - foreach_bfd_transport (F) -#undef F - BFD_OUTPUT_N_NEXT, -} bfd_output_next_t; - -static u32 bfd_next_index_by_transport[] = { -#define F(t, n) [BFD_TRANSPORT_##t] = BFD_OUTPUT_##t, +/* set to 0 here, real values filled at startup */ +static u32 bfd_node_index_by_transport[] = { +#define F(t, n) [BFD_TRANSPORT_##t] = 0, foreach_bfd_transport (F) #undef F }; @@ -378,7 +371,7 @@ bfd_input_format_trace (u8 * s, va_list * args) clib_net_to_host_u32 (pkt->des_min_tx)); s = format (s, " required min rx interval: %u\n", clib_net_to_host_u32 (pkt->req_min_rx)); - s = format (s, " required min echo rx interval: %u\n", + s = format (s, " required min echo rx interval: %u", clib_net_to_host_u32 (pkt->req_min_echo_rx)); } } @@ -426,10 +419,12 @@ bfd_add_transport_layer (vlib_main_t * vm, vlib_buffer_t * b, switch (bs->transport) { case BFD_TRANSPORT_UDP4: - /* fallthrough */ + BFD_DBG ("Transport bfd via udp4, bs_idx=%u", bs->bs_idx); + bfd_add_udp4_transport (vm, b, &bs->udp); + break; case BFD_TRANSPORT_UDP6: - BFD_DBG ("Transport bfd via udp, bs_idx=%u", bs->bs_idx); - bfd_add_udp_transport (vm, b, &bs->udp); + BFD_DBG ("Transport bfd via udp6, bs_idx=%u", bs->bs_idx); + bfd_add_udp6_transport (vm, b, &bs->udp); break; } } @@ -448,17 +443,14 @@ bfd_create_frame (vlib_main_t * vm, vlib_node_runtime_t * rt, vlib_buffer_t *b = vlib_get_buffer (vm, bi); ASSERT (b->current_data == 0); - u32 *to_next; - u32 n_left_to_next; - - vlib_get_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], - to_next, n_left_to_next); + vlib_frame_t *f = + vlib_get_frame_to_node (vm, bfd_node_index_by_transport[bs->transport]); + u32 *to_next = vlib_frame_vector_args (f); to_next[0] = bi; - n_left_to_next -= 1; + f->n_vectors = 1; - vlib_put_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], - n_left_to_next); + vlib_put_frame_to_node (vm, bfd_node_index_by_transport[bs->transport], f); return b; } @@ -680,13 +672,8 @@ VLIB_REGISTER_NODE (bfd_process_node, static) = { .function = bfd_process, .type = VLIB_NODE_TYPE_PROCESS, .name = "bfd-process", - .n_next_nodes = BFD_OUTPUT_N_NEXT, - .next_nodes = - { -#define F(t, n) [BFD_OUTPUT_##t] = n, - foreach_bfd_transport (F) -#undef F - }, + .n_next_nodes = 0, + .next_nodes = {}, }; /* *INDENT-ON* */ @@ -734,6 +721,13 @@ bfd_main_init (vlib_main_t * vm) timing_wheel_init (&bm->wheel, now, bm->cpu_cps); bm->wheel_inaccuracy = 2 << bm->wheel.log2_clocks_per_bin; + vlib_node_t *node = NULL; +#define F(t, n) \ + node = vlib_get_node_by_name (vm, (u8 *)n); \ + bfd_node_index_by_transport[BFD_TRANSPORT_##t] = node->index;\ + BFD_DBG("node '%s' has index %u", n, node->index); + foreach_bfd_transport (F); +#undef F return 0; } diff --git a/src/vnet/bfd/bfd_main.h b/src/vnet/bfd/bfd_main.h index cc82c839..20da381a 100644 --- a/src/vnet/bfd/bfd_main.h +++ b/src/vnet/bfd/bfd_main.h @@ -25,7 +25,7 @@ #include #define foreach_bfd_transport(F) \ - F (UDP4, "ip4-rewrite") \ + F (UDP4, "ip4-rewrite") \ F (UDP6, "ip6-rewrite") typedef enum diff --git a/src/vnet/bfd/bfd_udp.c b/src/vnet/bfd/bfd_udp.c index c1596bf6..fe348404 100644 --- a/src/vnet/bfd/bfd_udp.c +++ b/src/vnet/bfd/bfd_udp.c @@ -31,53 +31,80 @@ static vlib_node_registration_t bfd_udp6_input_node; bfd_udp_main_t bfd_udp_main; -void bfd_udp_transport_to_buffer (vlib_main_t *vm, vlib_buffer_t *b, - bfd_udp_session_t *bus) +void bfd_add_udp4_transport (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) { udp_header_t *udp; - u16 udp_length, ip_length; - bfd_udp_key_t *key = &bus->key; + const bfd_udp_key_t *key = &bus->key; b->flags |= VNET_BUFFER_LOCALLY_ORIGINATED; - if (ip46_address_is_ip4 (&key->local_addr)) - { - ip4_header_t *ip4; - const size_t data_size = sizeof (*ip4) + sizeof (*udp); - vlib_buffer_advance (b, -data_size); - ip4 = vlib_buffer_get_current (b); - udp = (udp_header_t *)(ip4 + 1); - memset (ip4, 0, data_size); - ip4->ip_version_and_header_length = 0x45; - ip4->ttl = 255; - ip4->protocol = IP_PROTOCOL_UDP; - ip4->src_address.as_u32 = key->local_addr.ip4.as_u32; - ip4->dst_address.as_u32 = key->peer_addr.ip4.as_u32; - - udp->src_port = clib_host_to_net_u16 (50000); /* FIXME */ - udp->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_bfd4); - - /* fix ip length, checksum and udp length */ - ip_length = vlib_buffer_length_in_chain (vm, b); - - ip4->length = clib_host_to_net_u16 (ip_length); - ip4->checksum = ip4_header_checksum (ip4); - - udp_length = ip_length - (sizeof (*ip4)); - udp->length = clib_host_to_net_u16 (udp_length); - } - else - { - BFD_ERR ("not implemented"); - abort (); - } + vnet_buffer (b)->ip.adj_index[VLIB_RX] = bus->adj_index; + vnet_buffer (b)->ip.adj_index[VLIB_TX] = bus->adj_index; + ip4_header_t *ip4; + const size_t headers_size = sizeof (*ip4) + sizeof (*udp); + vlib_buffer_advance (b, -headers_size); + ip4 = vlib_buffer_get_current (b); + udp = (udp_header_t *)(ip4 + 1); + memset (ip4, 0, headers_size); + ip4->ip_version_and_header_length = 0x45; + ip4->ttl = 255; + ip4->protocol = IP_PROTOCOL_UDP; + ip4->src_address.as_u32 = key->local_addr.ip4.as_u32; + ip4->dst_address.as_u32 = key->peer_addr.ip4.as_u32; + + udp->src_port = clib_host_to_net_u16 (50000); /* FIXME */ + udp->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_bfd4); + + /* fix ip length, checksum and udp length */ + const u16 ip_length = vlib_buffer_length_in_chain (vm, b); + + ip4->length = clib_host_to_net_u16 (ip_length); + ip4->checksum = ip4_header_checksum (ip4); + + const u16 udp_length = ip_length - (sizeof (*ip4)); + udp->length = clib_host_to_net_u16 (udp_length); } -void bfd_add_udp_transport (vlib_main_t *vm, vlib_buffer_t *b, - bfd_udp_session_t *bus) +void bfd_add_udp6_transport (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) { + udp_header_t *udp; + const bfd_udp_key_t *key = &bus->key; + + b->flags |= VNET_BUFFER_LOCALLY_ORIGINATED; vnet_buffer (b)->ip.adj_index[VLIB_RX] = bus->adj_index; vnet_buffer (b)->ip.adj_index[VLIB_TX] = bus->adj_index; - bfd_udp_transport_to_buffer (vm, b, bus); + ip6_header_t *ip6; + const size_t headers_size = sizeof (*ip6) + sizeof (*udp); + vlib_buffer_advance (b, -headers_size); + ip6 = vlib_buffer_get_current (b); + udp = (udp_header_t *)(ip6 + 1); + memset (ip6, 0, headers_size); + ip6->ip_version_traffic_class_and_flow_label = + clib_host_to_net_u32 (0x6 << 28); + ip6->hop_limit = 255; + ip6->protocol = IP_PROTOCOL_UDP; + clib_memcpy (&ip6->src_address, &key->local_addr.ip6, + sizeof (ip6->src_address)); + clib_memcpy (&ip6->dst_address, &key->peer_addr.ip6, + sizeof (ip6->dst_address)); + + udp->src_port = clib_host_to_net_u16 (50000); /* FIXME */ + udp->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_bfd6); + + /* fix ip payload length and udp length */ + const u16 udp_length = vlib_buffer_length_in_chain (vm, b) - (sizeof (*ip6)); + udp->length = clib_host_to_net_u16 (udp_length); + ip6->payload_length = udp->length; + + /* IPv6 UDP checksum is mandatory */ + int bogus = 0; + udp->checksum = ip6_tcp_udp_icmp_compute_checksum (vm, b, ip6, &bogus); + ASSERT (bogus == 0); + if (udp->checksum == 0) + { + udp->checksum = 0xffff; + } } static bfd_session_t *bfd_lookup_session (bfd_udp_main_t *bum, @@ -345,29 +372,29 @@ static bfd_udp_error_t bfd_udp4_verify_transport (const ip4_header_t *ip4, const bfd_udp_key_t *key = &bus->key; if (ip4->src_address.as_u32 != key->peer_addr.ip4.as_u32) { - BFD_ERR ("IP src addr mismatch, got %U, expected %U", format_ip4_address, - ip4->src_address.as_u32, format_ip4_address, - key->peer_addr.ip4.as_u32); + BFD_ERR ("IPv4 src addr mismatch, got %U, expected %U", + format_ip4_address, ip4->src_address.as_u8, format_ip4_address, + key->peer_addr.ip4.as_u8); return BFD_UDP_ERROR_BAD; } if (ip4->dst_address.as_u32 != key->local_addr.ip4.as_u32) { - BFD_ERR ("IP dst addr mismatch, got %U, expected %U", format_ip4_address, - ip4->dst_address.as_u32, format_ip4_address, - key->local_addr.ip4.as_u32); + BFD_ERR ("IPv4 dst addr mismatch, got %U, expected %U", + format_ip4_address, ip4->dst_address.as_u8, format_ip4_address, + key->local_addr.ip4.as_u8); return BFD_UDP_ERROR_BAD; } const u8 expected_ttl = 255; if (ip4->ttl != expected_ttl) { - BFD_ERR ("IP unexpected TTL value %d, expected %d", ip4->ttl, + BFD_ERR ("IPv4 unexpected TTL value %u, expected %u", ip4->ttl, expected_ttl); return BFD_UDP_ERROR_BAD; } if (clib_net_to_host_u16 (udp->src_port) < 49152 || clib_net_to_host_u16 (udp->src_port) > 65535) { - BFD_ERR ("Invalid UDP src port %d, out of range <49152,65535>", + BFD_ERR ("Invalid UDP src port %u, out of range <49152,65535>", udp->src_port); } return BFD_UDP_ERROR_NONE; @@ -460,10 +487,128 @@ static bfd_udp_error_t bfd_udp4_scan (vlib_main_t *vm, vlib_node_runtime_t *rt, return BFD_UDP_ERROR_NONE; } -static bfd_udp_error_t bfd_udp6_scan (vlib_main_t *vm, vlib_buffer_t *b) +static void bfd_udp6_find_headers (vlib_buffer_t *b, const ip6_header_t **ip6, + const udp_header_t **udp) +{ + /* sanity check first */ + const i32 start = vnet_buffer (b)->ip.start_of_ip_header; + if (start < 0 && start < sizeof (b->pre_data)) + { + BFD_ERR ("Start of ip header is before pre_data, ignoring"); + *ip6 = NULL; + *udp = NULL; + return; + } + *ip6 = (ip6_header_t *)(b->data + start); + if ((u8 *)*ip6 > (u8 *)vlib_buffer_get_current (b)) + { + BFD_ERR ("Start of ip header is beyond current data, ignoring"); + *ip6 = NULL; + *udp = NULL; + return; + } + *udp = (udp_header_t *)((*ip6) + 1); +} + +static bfd_udp_error_t bfd_udp6_verify_transport (const ip6_header_t *ip6, + const udp_header_t *udp, + const bfd_session_t *bs) { - /* TODO */ - return BFD_UDP_ERROR_BAD; + const bfd_udp_session_t *bus = &bs->udp; + const bfd_udp_key_t *key = &bus->key; + if (ip6->src_address.as_u64[0] != key->peer_addr.ip6.as_u64[0] && + ip6->src_address.as_u64[1] != key->peer_addr.ip6.as_u64[1]) + { + BFD_ERR ("IP src addr mismatch, got %U, expected %U", format_ip6_address, + ip6, format_ip6_address, &key->peer_addr.ip6); + return BFD_UDP_ERROR_BAD; + } + if (ip6->dst_address.as_u64[0] != key->local_addr.ip6.as_u64[0] && + ip6->dst_address.as_u64[1] != key->local_addr.ip6.as_u64[1]) + { + BFD_ERR ("IP dst addr mismatch, got %U, expected %U", format_ip6_address, + ip6, format_ip6_address, &key->local_addr.ip6); + return BFD_UDP_ERROR_BAD; + } + const u8 expected_hop_limit = 255; + if (ip6->hop_limit != expected_hop_limit) + { + BFD_ERR ("IPv6 unexpected hop-limit value %u, expected %u", + ip6->hop_limit, expected_hop_limit); + return BFD_UDP_ERROR_BAD; + } + if (clib_net_to_host_u16 (udp->src_port) < 49152 || + clib_net_to_host_u16 (udp->src_port) > 65535) + { + BFD_ERR ("Invalid UDP src port %u, out of range <49152,65535>", + udp->src_port); + } + return BFD_UDP_ERROR_NONE; +} + +static bfd_udp_error_t bfd_udp6_scan (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_buffer_t *b, bfd_session_t **bs_out) +{ + const bfd_pkt_t *pkt = vlib_buffer_get_current (b); + if (sizeof (*pkt) > b->current_length) + { + BFD_ERR ( + "Payload size %d too small to hold bfd packet of minimum size %d", + b->current_length, sizeof (*pkt)); + return BFD_UDP_ERROR_BAD; + } + const ip6_header_t *ip6; + const udp_header_t *udp; + bfd_udp6_find_headers (b, &ip6, &udp); + if (!ip6 || !udp) + { + BFD_ERR ("Couldn't find ip6 or udp header"); + return BFD_UDP_ERROR_BAD; + } + if (!bfd_verify_pkt_common (pkt)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_session_t *bs = NULL; + if (pkt->your_disc) + { + BFD_DBG ("Looking up BFD session using discriminator %u", + pkt->your_disc); + bs = bfd_find_session_by_disc (bfd_udp_main.bfd_main, pkt->your_disc); + } + else + { + bfd_udp_key_t key; + memset (&key, 0, sizeof (key)); + key.sw_if_index = vnet_buffer (b)->sw_if_index[VLIB_RX]; + key.local_addr.ip6.as_u64[0] = ip6->dst_address.as_u64[0]; + key.local_addr.ip6.as_u64[1] = ip6->dst_address.as_u64[1]; + key.peer_addr.ip6.as_u64[0] = ip6->src_address.as_u64[0]; + key.peer_addr.ip6.as_u64[1] = ip6->src_address.as_u64[1]; + BFD_DBG ("Looking up BFD session using key (sw_if_index=%u, local=%U, " + "peer=%U)", + key.sw_if_index, format_ip6_address, &key.local_addr, + format_ip6_address, &key.peer_addr); + bs = bfd_lookup_session (&bfd_udp_main, &key); + } + if (!bs) + { + BFD_ERR ("BFD session lookup failed - no session matches BFD pkt"); + return BFD_UDP_ERROR_BAD; + } + BFD_DBG ("BFD session found, bs_idx=%u", bs->bs_idx); + if (!bfd_verify_pkt_session (pkt, b->current_length, bs)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_udp_error_t err; + if (BFD_UDP_ERROR_NONE != (err = bfd_udp6_verify_transport (ip6, udp, bs))) + { + return err; + } + bfd_rpc_update_session (bs->bs_idx, pkt); + *bs_out = bs; + return BFD_UDP_ERROR_NONE; } /* @@ -504,7 +649,7 @@ static uword bfd_udp_input (vlib_main_t *vm, vlib_node_runtime_t *rt, /* scan this bfd pkt. error0 is the counter index to bmp */ if (is_ipv6) { - error0 = bfd_udp6_scan (vm, b0); + error0 = bfd_udp6_scan (vm, rt, b0, &bs); } else { diff --git a/src/vnet/bfd/bfd_udp.h b/src/vnet/bfd/bfd_udp.h index 51f5327b..2cd89ca2 100644 --- a/src/vnet/bfd/bfd_udp.h +++ b/src/vnet/bfd/bfd_udp.h @@ -42,8 +42,10 @@ typedef struct adj_index_t adj_index; } bfd_udp_session_t; -void bfd_add_udp_transport (vlib_main_t * vm, vlib_buffer_t * b, - bfd_udp_session_t * bs); +void bfd_add_udp4_transport (vlib_main_t * vm, vlib_buffer_t * b, + bfd_udp_session_t * bs); +void bfd_add_udp6_transport (vlib_main_t * vm, vlib_buffer_t * b, + bfd_udp_session_t * bs); #endif /* __included_bfd_udp_h__ */ diff --git a/test/bfd.py b/test/bfd.py index 57a5bd86..51716813 100644 --- a/test/bfd.py +++ b/test/bfd.py @@ -111,14 +111,24 @@ class VppBFDUDPSession(VppObject): def local_addr(self): """ BFD session local address (VPP address) """ if self._local_addr is None: - return self._interface.local_ip4 + if self.af == AF_INET: + return self._interface.local_ip4 + elif self.af == AF_INET6: + return self._interface.local_ip6 + else: + raise Exception("Unexpected af %s' % af" % self.af) return self._local_addr @property def local_addr_n(self): """ BFD session local address (VPP address) - raw, suitable for API """ if self._local_addr is None: - return self._interface.local_ip4n + if self.af == AF_INET: + return self._interface.local_ip4n + elif self.af == AF_INET6: + return self._interface.local_ip6n + else: + raise Exception("Unexpected af %s' % af" % self.af) return self._local_addr_n @property diff --git a/test/test_bfd.py b/test/test_bfd.py index b7832247..b56df339 100644 --- a/test/test_bfd.py +++ b/test/test_bfd.py @@ -62,36 +62,14 @@ class BFDAPITestCase(VppTestCase): session1.bs_index) -def create_packet(interface, ttl=255, src_port=50000, **kwargs): - p = (Ether(src=interface.remote_mac, dst=interface.local_mac) / - IP(src=interface.remote_ip4, dst=interface.local_ip4, ttl=ttl) / - UDP(sport=src_port, dport=BFD.udp_dport) / - BFD(*kwargs)) - return p - - -def verify_ip(test, packet, local_ip, remote_ip): - """ Verify correctness of IP layer. """ - ip = packet[IP] - test.assert_equal(ip.src, local_ip, "IP source address") - test.assert_equal(ip.dst, remote_ip, "IP destination address") - test.assert_equal(ip.ttl, 255, "IP TTL") - - -def verify_udp(test, packet): - """ Verify correctness of UDP layer. """ - udp = packet[UDP] - test.assert_equal(udp.dport, BFD.udp_dport, "UDP destination port") - test.assert_in_range(udp.sport, BFD.udp_sport_min, BFD.udp_sport_max, - "UDP source port") - - class BFDTestSession(object): """ BFD session as seen from test framework side """ - def __init__(self, test, interface, detect_mult=3): + def __init__(self, test, interface, af, detect_mult=3): self.test = test + self.af = af self.interface = interface + self.udp_sport = 50000 self.bfd_values = { 'my_discriminator': 0, 'desired_min_tx_interval': 100000, @@ -103,7 +81,22 @@ class BFDTestSession(object): self.bfd_values.update(kwargs) def create_packet(self): - packet = create_packet(self.interface) + if self.af == AF_INET6: + packet = (Ether(src=self.interface.remote_mac, + dst=self.interface.local_mac) / + IPv6(src=self.interface.remote_ip6, + dst=self.interface.local_ip6, + hlim=255) / + UDP(sport=self.udp_sport, dport=BFD.udp_dport) / + BFD()) + else: + packet = (Ether(src=self.interface.remote_mac, + dst=self.interface.local_mac) / + IP(src=self.interface.remote_ip4, + dst=self.interface.local_ip4, + ttl=255) / + UDP(sport=self.udp_sport, dport=BFD.udp_dport) / + BFD()) self.test.logger.debug("BFD: Creating packet") for name, value in self.bfd_values.iteritems(): self.test.logger.debug("BFD: setting packet.%s=%s", name, value) @@ -125,41 +118,52 @@ class BFDTestSession(object): "BFD - your discriminator") -@unittest.skip("") -class BFDTestCase(VppTestCase): - """Bidirectional Forwarding Detection (BFD)""" - - @classmethod - def setUpClass(cls): - super(BFDTestCase, cls).setUpClass() - try: - cls.create_pg_interfaces([0]) - cls.pg0.config_ip4() - cls.pg0.generate_remote_hosts() - cls.pg0.configure_ipv4_neighbors() - cls.pg0.admin_up() - cls.pg0.resolve_arp() - - except Exception: - super(BFDTestCase, cls).tearDownClass() - raise - - def setUp(self): - super(BFDTestCase, self).setUp() - self.vapi.want_bfd_events() - self.vpp_session = VppBFDUDPSession( - self, self.pg0, self.pg0.remote_ip4) - self.vpp_session.add_vpp_config() - self.vpp_session.admin_up() - self.test_session = BFDTestSession(self, self.pg0) - self.test_session.update(required_min_rx_interval=100000) +class BFDCommonCode: + """Common code used by both IPv4 and IPv6 Test Cases""" def tearDown(self): self.vapi.collect_events() # clear the event queue if not self.vpp_dead: self.vapi.want_bfd_events(enable_disable=0) self.vpp_session.remove_vpp_config() - super(BFDTestCase, self).tearDown() + + def bfd_session_up(self): + self.pg_enable_capture([self.pg0]) + self.logger.info("BFD: Waiting for slow hello") + p, timeout = self.wait_for_bfd_packet(2) + self.logger.info("BFD: Sending Init") + self.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init, + required_min_rx_interval=100000) + self.test_session.send_packet() + self.logger.info("BFD: Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.up) + self.logger.info("BFD: Session is Up") + self.test_session.update(state=BFDState.up) + + def verify_ip(self, packet): + """ Verify correctness of IP layer. """ + if self.vpp_session.af == AF_INET6: + ip = packet[IPv6] + local_ip = self.pg0.local_ip6 + remote_ip = self.pg0.remote_ip6 + self.assert_equal(ip.hlim, 255, "IPv6 hop limit") + else: + ip = packet[IP] + local_ip = self.pg0.local_ip4 + remote_ip = self.pg0.remote_ip4 + self.assert_equal(ip.ttl, 255, "IPv4 TTL") + self.assert_equal(ip.src, local_ip, "IP source address") + self.assert_equal(ip.dst, remote_ip, "IP destination address") + + def verify_udp(self, packet): + """ Verify correctness of UDP layer. """ + udp = packet[UDP] + self.assert_equal(udp.dport, BFD.udp_dport, "UDP destination port") + self.assert_in_range(udp.sport, BFD.udp_sport_min, BFD.udp_sport_max, + "UDP source port") def verify_event(self, event, expected_state): """ Verify correctness of event values. """ @@ -198,35 +202,64 @@ class BFDTestCase(VppTestCase): before = time.time() p = self.pg0.wait_for_packet(timeout=timeout) after = time.time() + self.logger.debug(ppp("Got packet:", p)) bfd = p[BFD] if bfd is None: raise Exception(ppp("Unexpected or invalid BFD packet:", p)) if bfd.payload: raise Exception(ppp("Unexpected payload in BFD packet:", bfd)) - verify_ip(self, p, self.pg0.local_ip4, self.pg0.remote_ip4) - verify_udp(self, p) + self.verify_ip(p) + self.verify_udp(p) self.test_session.verify_packet(p) return p, after - before - def bfd_session_up(self): - self.pg_enable_capture([self.pg0]) - self.logger.info("BFD: Waiting for slow hello") - p, ttp = self.wait_for_bfd_packet() - self.logger.info("BFD: Sending Init") - self.test_session.update(my_discriminator=randint(0, 40000000), - your_discriminator=p[BFD].my_discriminator, - state=BFDState.init) - self.test_session.send_packet() - self.logger.info("BFD: Waiting for event") - e = self.vapi.wait_for_event(1, "bfd_udp_session_details") - self.verify_event(e, expected_state=BFDState.up) - self.logger.info("BFD: Session is Up") - self.test_session.update(state=BFDState.up) - def test_session_up(self): """ bring BFD session up """ self.bfd_session_up() + def test_hold_up(self): + """ hold BFD session up """ + self.bfd_session_up() + for i in range(5): + self.wait_for_bfd_packet() + self.test_session.send_packet() + + +class BFD4TestCase(VppTestCase, BFDCommonCode): + """Bidirectional Forwarding Detection (BFD)""" + + @classmethod + def setUpClass(cls): + super(BFD4TestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip4() + cls.pg0.generate_remote_hosts() + cls.pg0.configure_ipv4_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFD4TestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFD4TestCase, self).setUp() + self.vapi.want_bfd_events() + try: + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0, AF_INET) + except: + self.vapi.want_bfd_events(enable_disable=0) + raise + + def tearDown(self): + BFDCommonCode.tearDown(self) + super(BFD4TestCase, self).tearDown() + def test_slow_timer(self): """ verify slow periodic control frames while session down """ self.pg_enable_capture([self.pg0]) @@ -261,13 +294,6 @@ class BFDTestCase(VppTestCase): return raise Exception(ppp("Received unexpected BFD packet:", p)) - def test_hold_up(self): - """ hold BFD session up """ - self.bfd_session_up() - for i in range(5): - self.wait_for_bfd_packet() - self.test_session.send_packet() - def test_conn_down(self): """ verify session goes down after inactivity """ self.bfd_session_up() @@ -324,5 +350,42 @@ class BFDTestCase(VppTestCase): 1.10 * interval / us_in_sec, "time between BFD packets") + +class BFD6TestCase(VppTestCase, BFDCommonCode): + """Bidirectional Forwarding Detection (BFD) (IPv6) """ + + @classmethod + def setUpClass(cls): + super(BFD6TestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip6() + cls.pg0.configure_ipv6_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_ndp() + + except Exception: + super(BFD6TestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFD6TestCase, self).setUp() + self.vapi.want_bfd_events() + try: + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip6, + af=AF_INET6) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0, AF_INET6) + self.logger.debug(self.vapi.cli("show adj nbr")) + except: + self.vapi.want_bfd_events(enable_disable=0) + raise + + def tearDown(self): + BFDCommonCode.tearDown(self) + super(BFD6TestCase, self).tearDown() + if __name__ == '__main__': unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index 79893602..24e9af44 100644 --- a/test/util.py +++ b/test/util.py @@ -76,19 +76,24 @@ class Host(object): @property def ip4(self): - """ IPv4 address """ + """ IPv4 address - string """ return self._ip4 @property def ip4n(self): - """ IPv4 address """ + """ IPv4 address of remote host - raw, suitable as API parameter.""" return socket.inet_pton(socket.AF_INET, self._ip4) @property def ip6(self): - """ IPv6 address """ + """ IPv6 address - string """ return self._ip6 + @property + def ip6n(self): + """ IPv6 address of remote host - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET6, self._ip6) + def __init__(self, mac=None, ip4=None, ip6=None): self._mac = mac self._ip4 = ip4 diff --git a/test/vpp_interface.py b/test/vpp_interface.py index e0a29f94..ee4a9ef6 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -131,7 +131,7 @@ class VppInterface(object): 2, count + 2): # 0: network address, 1: local vpp address mac = "02:%02x:00:00:ff:%02x" % (self.sw_if_index, i) ip4 = "172.16.%u.%u" % (self.sw_if_index, i) - ip6 = "fd01:%04x::%04x" % (self.sw_if_index, i) + ip6 = "fd01:%x::%x" % (self.sw_if_index, i) host = Host(mac, ip4, ip6) self._remote_hosts.append(host) self._hosts_by_mac[mac] = host @@ -155,7 +155,7 @@ class VppInterface(object): self.has_ip4_config = False self.ip4_table_id = 0 - self._local_ip6 = "fd01:%04x::1" % self.sw_if_index + self._local_ip6 = "fd01:%x::1" % self.sw_if_index self._local_ip6n = socket.inet_pton(socket.AF_INET6, self.local_ip6) self.local_ip6_prefix_len = 64 self.has_ip6_config = False @@ -226,6 +226,13 @@ class VppInterface(object): self.has_ip6_config = False self.has_ip6_config = False + def configure_ipv6_neighbors(self): + """For every remote host assign neighbor's MAC to IPv6 address.""" + for host in self._remote_hosts: + macn = host.mac.replace(":", "").decode('hex') + self.test.vapi.ip_neighbor_add_del( + self.sw_if_index, macn, host.ip6n, is_ipv6=1) + def unconfig(self): """Unconfigure IPv6 and IPv4 address on the VPP interface.""" self.unconfig_ip4() -- cgit 1.2.3-korg From acb9b8e8c3394d06964ad0f8387b764c01f43152 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 14 Feb 2017 02:55:31 +0100 Subject: make test: improve stability Disable automatic garbage collection and run it manually before running each test case to minimize stalls. Improve vpp subprocess cleanup. Reduce helper thread count to one and properly clean that thread once it's not needed. Change-Id: I3ea78ed9628552b5ef3ff29cc7bcf2d3fc42f2c3 Signed-off-by: Klement Sekera --- test/Makefile | 11 ++++- test/framework.py | 78 ++++++++++++++++++++++++++--------- test/util.py | 2 + test/vpp_object.py | 35 ++++++++-------- test/vpp_papi_provider.py | 8 +++- test/vpp_pg_interface.py | 103 +++++++++++++++++++++++++--------------------- 6 files changed, 149 insertions(+), 88 deletions(-) (limited to 'test/util.py') diff --git a/test/Makefile b/test/Makefile index 5c0d48f0..65b5a9bd 100644 --- a/test/Makefile +++ b/test/Makefile @@ -5,8 +5,14 @@ ifndef VPP_PYTHON_PREFIX $(error VPP_PYTHON_PREFIX is not set) endif +UNITTEST_EXTRA_OPTS="" + +ifeq ($(FAILFAST),1) +UNITTEST_EXTRA_OPTS="-f" +endif + PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=scapy==2.3.3 pexpect +PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 SCAPY_SOURCE=$(PYTHON_VENV_PATH)/lib/python2.7/site-packages/ BUILD_COV_DIR = $(BR)/test-cov @@ -35,7 +41,7 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_\"*.py\"" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover $(UNITTEST_EXTRA_OPTS) -p test_\"*.py\"" endef test: reset verify-python-path $(PAPI_INSTALL_DONE) @@ -103,6 +109,7 @@ help: @echo "" @echo "Arguments controlling test runs:" @echo " V=[0|1|2] - set test verbosity level" + @echo " FAILFAST=[0|1] - fail fast if 1, complete all tests if 0" @echo " DEBUG= - set VPP debugging kind" @echo " DEBUG=core - detect coredump and load it in gdb on crash" @echo " DEBUG=gdb - allow easy debugging by printing VPP PID " diff --git a/test/framework.py b/test/framework.py index 6a0ec965..8dd61aa1 100644 --- a/test/framework.py +++ b/test/framework.py @@ -1,23 +1,33 @@ #!/usr/bin/env python -import subprocess +from __future__ import print_function +import gc +import sys +import os +import select import unittest import tempfile import time import resource from collections import deque -from threading import Thread +from threading import Thread, Event from inspect import getdoc from traceback import format_exception +from logging import FileHandler, DEBUG, Formatter +from scapy.packet import Raw from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface from vpp_sub_interface import VppSubInterface from vpp_lo_interface import VppLoInterface from vpp_papi_provider import VppPapiProvider -from scapy.packet import Raw -from logging import FileHandler, DEBUG from log import * from vpp_object import VppObjectRegistry +if os.name == 'posix' and sys.version_info[0] < 3: + # using subprocess32 is recommended by python official documentation + # @ https://docs.python.org/2/library/subprocess.html + import subprocess32 as subprocess +else: + import subprocess """ Test framework module. @@ -51,9 +61,21 @@ class _PacketInfo(object): return index and src and dst and data -def pump_output(out, deque): - for line in iter(out.readline, b''): - deque.append(line) +def pump_output(testclass): + """ pump output from vpp stdout/stderr to proper queues """ + while not testclass.pump_thread_stop_flag.wait(0): + readable = select.select([testclass.vpp.stdout.fileno(), + testclass.vpp.stderr.fileno(), + testclass.pump_thread_wakeup_pipe[0]], + [], [])[0] + if testclass.vpp.stdout.fileno() in readable: + read = os.read(testclass.vpp.stdout.fileno(), 1024) + testclass.vpp_stdout_deque.append(read) + if testclass.vpp.stderr.fileno() in readable: + read = os.read(testclass.vpp.stderr.fileno(), 1024) + testclass.vpp_stderr_deque.append(read) + # ignoring the dummy pipe here intentionally - the flag will take care + # of properly terminating the loop class VppTestCase(unittest.TestCase): @@ -181,10 +203,14 @@ class VppTestCase(unittest.TestCase): Perform class setup before running the testcase Remove shared memory files, start vpp and connect the vpp-api """ + gc.collect() # run garbage collection first cls.logger = getLogger(cls.__name__) cls.tempdir = tempfile.mkdtemp( prefix='vpp-unittest-' + cls.__name__ + '-') file_handler = FileHandler("%s/log.txt" % cls.tempdir) + file_handler.setFormatter( + Formatter(fmt='%(asctime)s,%(msecs)03d %(message)s', + datefmt="%H:%M:%S")) file_handler.setLevel(DEBUG) cls.logger.addHandler(file_handler) cls.shm_prefix = cls.tempdir.split("/")[-1] @@ -206,20 +232,18 @@ class VppTestCase(unittest.TestCase): try: cls.run_vpp() cls.vpp_stdout_deque = deque() - cls.vpp_stdout_reader_thread = Thread(target=pump_output, args=( - cls.vpp.stdout, cls.vpp_stdout_deque)) - cls.vpp_stdout_reader_thread.start() cls.vpp_stderr_deque = deque() - cls.vpp_stderr_reader_thread = Thread(target=pump_output, args=( - cls.vpp.stderr, cls.vpp_stderr_deque)) - cls.vpp_stderr_reader_thread.start() + cls.pump_thread_stop_flag = Event() + cls.pump_thread_wakeup_pipe = os.pipe() + cls.pump_thread = Thread(target=pump_output, args=(cls,)) + cls.pump_thread.start() cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls) if cls.step: hook = StepHook(cls) else: hook = PollHook(cls) cls.vapi.register_hook(hook) - time.sleep(0.1) + cls.sleep(0.1, "after vpp startup, before initial poll") hook.poll_vpp() try: cls.vapi.connect() @@ -251,12 +275,25 @@ class VppTestCase(unittest.TestCase): raw_input("When done debugging, press ENTER to kill the " "process and finish running the testcase...") + os.write(cls.pump_thread_wakeup_pipe[1], 'ding dong wake up') + cls.pump_thread_stop_flag.set() + if hasattr(cls, 'pump_thread'): + cls.logger.debug("Waiting for pump thread to stop") + cls.pump_thread.join() + if hasattr(cls, 'vpp_stderr_reader_thread'): + cls.logger.debug("Waiting for stdderr pump to stop") + cls.vpp_stderr_reader_thread.join() + if hasattr(cls, 'vpp'): if hasattr(cls, 'vapi'): cls.vapi.disconnect() + del cls.vapi cls.vpp.poll() if cls.vpp.returncode is None: + cls.logger.debug("Sending TERM to vpp") cls.vpp.terminate() + cls.logger.debug("Waiting for vpp to die") + cls.vpp.communicate() del cls.vpp if hasattr(cls, 'vpp_stdout_deque'): @@ -306,7 +343,7 @@ class VppTestCase(unittest.TestCase): self._testMethodDoc)) if self.vpp_dead: raise Exception("VPP is dead when setting up the test") - time.sleep(.1) + self.sleep(.1, "during setUp") self.vpp_stdout_deque.append( "--- test setUp() for %s.%s(%s) starts here ---\n" % (self.__class__.__name__, self._testMethodName, @@ -351,9 +388,7 @@ class VppTestCase(unittest.TestCase): for stamp, cap_name in cls._zombie_captures: wait = stamp + capture_ttl - now if wait > 0: - cls.logger.debug("Waiting for %ss before deleting capture %s", - wait, cap_name) - time.sleep(wait) + cls.sleep(wait, "before deleting capture %s" % cap_name) now = time.time() cls.logger.debug("Removing zombie capture %s" % cap_name) cls.vapi.cli('packet-generator delete %s' % cap_name) @@ -552,8 +587,10 @@ class VppTestCase(unittest.TestCase): name, real_value, expected_min, expected_max) self.assertTrue(expected_min <= real_value <= expected_max, msg) - def sleep(self, timeout): - self.logger.debug("Sleeping for %ss" % timeout) + @classmethod + def sleep(cls, timeout, remark=None): + if hasattr(cls, 'logger'): + cls.logger.debug("Sleeping for %ss (%s)" % (timeout, remark)) time.sleep(timeout) @@ -817,6 +854,7 @@ class VppTestRunner(unittest.TextTestRunner): :param test: """ + gc.disable() # disable garbage collection, we'll do that manually print("Running tests using custom test runner") # debug message filter_file, filter_class, filter_func = self.parse_test_option() print("Active filters: file=%s, class=%s, function=%s" % ( diff --git a/test/util.py b/test/util.py index 24e9af44..a6484906 100644 --- a/test/util.py +++ b/test/util.py @@ -1,3 +1,5 @@ +""" test framework utilities """ + import socket import sys from abc import abstractmethod, ABCMeta diff --git a/test/vpp_object.py b/test/vpp_object.py index 1997bf55..0d74baa5 100644 --- a/test/vpp_object.py +++ b/test/vpp_object.py @@ -1,3 +1,5 @@ +""" abstract vpp object and object registry """ + from abc import ABCMeta, abstractmethod @@ -5,9 +7,6 @@ class VppObject(object): """ Abstract vpp object """ __metaclass__ = ABCMeta - def __init__(self): - VppObjectRegistry().register(self) - @abstractmethod def add_vpp_config(self): """ Add the configuration for this object to vpp. """ @@ -42,13 +41,13 @@ class VppObjectRegistry(object): if not hasattr(self, "_object_dict"): self._object_dict = dict() - def register(self, o, logger): + def register(self, obj, logger): """ Register an object in the registry. """ - if not o.object_id() in self._object_dict: - self._object_registry.append(o) - self._object_dict[o.object_id()] = o + if obj.object_id() not in self._object_dict: + self._object_registry.append(obj) + self._object_dict[obj.object_id()] = obj else: - logger.debug("REG: duplicate add, ignoring (%s)" % o) + logger.debug("REG: duplicate add, ignoring (%s)" % obj) def remove_vpp_config(self, logger): """ @@ -60,23 +59,23 @@ class VppObjectRegistry(object): return logger.info("REG: Removing VPP configuration for registered objects") # remove the config in reverse order as there might be dependencies - for o in reversed(self._object_registry): - if o.query_vpp_config(): - logger.info("REG: Removing configuration for %s" % o) - o.remove_vpp_config() + for obj in reversed(self._object_registry): + if obj.query_vpp_config(): + logger.info("REG: Removing configuration for %s" % obj) + obj.remove_vpp_config() else: logger.info( "REG: Skipping removal for %s, configuration not present" % - o) + obj) failed = [] - for o in self._object_registry: - if o.query_vpp_config(): - failed.append(o) + for obj in self._object_registry: + if obj.query_vpp_config(): + failed.append(obj) self._object_registry = [] self._object_dict = dict() if failed: logger.error("REG: Couldn't remove configuration for object(s):") - for x in failed: - logger.error(repr(x)) + for obj in failed: + logger.error(repr(obj)) raise Exception("Couldn't remove configuration for object(s): %s" % (", ".join(str(x) for x in failed))) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 39efa9e4..7a508a44 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -87,6 +87,12 @@ class VppPapiProvider(object): def wait_for_event(self, timeout, name=None): """ Wait for and return next event. """ + if name: + self.test_class.logger.debug("Expecting event within %ss", + timeout) + else: + self.test_class.logger.debug("Expecting event '%s' within %ss", + name, timeout) if self._events: self.test_class.logger.debug("Not waiting, event already queued") limit = time.time() + timeout @@ -101,8 +107,6 @@ class VppPapiProvider(object): (name, e)) return e time.sleep(0) # yield - if name is not None: - raise Exception("Event %s did not occur within timeout" % name) raise Exception("Event did not occur within timeout") def __call__(self, name, event): diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index d6e66849..4707f0b7 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -16,6 +16,11 @@ from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ismaddr from scapy.utils import inet_pton, inet_ntop +class CaptureTimeoutError(Exception): + """ Exception raised if capture or packet doesn't appear within timeout """ + pass + + def is_ipv6_misc(p): """ Is packet one of uninteresting IPv6 broadcasts? """ if p.haslayer(ICMPv6ND_RA): @@ -103,13 +108,15 @@ class VppPGInterface(VppInterface): """ Enable capture on this packet-generator interface""" try: if os.path.isfile(self.out_path): - os.rename(self.out_path, - "%s/history.[timestamp:%f].[%s-counter:%04d].%s" % - (self.test.tempdir, - time.time(), - self.name, - self.out_history_counter, - self._out_file)) + name = "%s/history.[timestamp:%f].[%s-counter:%04d].%s" % \ + (self.test.tempdir, + time.time(), + self.name, + self.out_history_counter, + self._out_file) + self.test.logger.debug("Renaming %s->%s" % + (self.out_path, name)) + os.rename(self.out_path, name) except: pass # FIXME this should be an API, but no such exists atm @@ -125,13 +132,15 @@ class VppPGInterface(VppInterface): """ try: if os.path.isfile(self.in_path): - os.rename(self.in_path, - "%s/history.[timestamp:%f].[%s-counter:%04d].%s" % - (self.test.tempdir, - time.time(), - self.name, - self.in_history_counter, - self._in_file)) + name = "%s/history.[timestamp:%f].[%s-counter:%04d].%s" %\ + (self.test.tempdir, + time.time(), + self.name, + self.in_history_counter, + self._in_file) + self.test.logger.debug("Renaming %s->%s" % + (self.in_path, name)) + os.rename(self.in_path, name) except: pass wrpcap(self.in_path, pkts) @@ -263,57 +272,50 @@ class VppPGInterface(VppInterface): :returns: True/False if the file is present or appears within timeout """ - limit = time.time() + timeout + deadline = time.time() + timeout if not os.path.isfile(self.out_path): self.test.logger.debug("Waiting for capture file %s to appear, " "timeout is %ss" % (self.out_path, timeout)) else: - self.test.logger.debug( - "Capture file %s already exists" % - self.out_path) + self.test.logger.debug("Capture file %s already exists" % + self.out_path) return True - while time.time() < limit: + while time.time() < deadline: if os.path.isfile(self.out_path): break time.sleep(0) # yield if os.path.isfile(self.out_path): self.test.logger.debug("Capture file appeared after %fs" % - (time.time() - (limit - timeout))) + (time.time() - (deadline - timeout))) else: self.test.logger.debug("Timeout - capture file still nowhere") return False return True - def wait_for_packet_data(self, deadline): + def verify_enough_packet_data_in_pcap(self): """ - Wait until enough data is available in the file handled by internal - pcap reader so that a whole packet can be read. + Check if enough data is available in file handled by internal pcap + reader so that a whole packet can be read. - :param deadline: timestamp by which the data must arrive - :raises Exception: if not enough data by deadline + :returns: True if enough data present, else False """ orig_pos = self._pcap_reader.f.tell() # save file position enough_data = False - while time.time() < deadline: - # read packet header from pcap - hdr = self._pcap_reader.f.read(16) - if len(hdr) < 16: - time.sleep(0) # yield - continue # cannot read full header, continue looping - # find the capture length - caplen + # read packet header from pcap + packet_header_size = 16 + caplen = None + end_pos = None + hdr = self._pcap_reader.f.read(packet_header_size) + if len(hdr) == packet_header_size: + # parse the capture length - caplen sec, usec, caplen, wirelen = struct.unpack( self._pcap_reader.endian + "IIII", hdr) self._pcap_reader.f.seek(0, 2) # seek to end of file end_pos = self._pcap_reader.f.tell() # get position at end if end_pos >= orig_pos + len(hdr) + caplen: enough_data = True # yay, we have enough data - break - self.test.logger.debug("Partial packet data in pcap") - time.sleep(0) # yield self._pcap_reader.f.seek(orig_pos, 0) # restore original position - if not enough_data: - raise Exception( - "Not enough data to read a full packet within deadline") + return enough_data def wait_for_packet(self, timeout, filter_out_fn=is_ipv6_misc): """ @@ -327,8 +329,8 @@ class VppPGInterface(VppInterface): deadline = time.time() + timeout if self._pcap_reader is None: if not self.wait_for_capture_file(timeout): - raise Exception("Capture file %s did not appear within " - "timeout" % self.out_path) + raise CaptureTimeoutError("Capture file %s did not appear " + "within timeout" % self.out_path) while time.time() < deadline: try: self._pcap_reader = PcapReader(self.out_path) @@ -338,12 +340,20 @@ class VppPGInterface(VppInterface): "Exception in scapy.PcapReader(%s): %s" % (self.out_path, format_exc())) if not self._pcap_reader: - raise Exception("Capture file %s did not appear within " - "timeout" % self.out_path) + raise CaptureTimeoutError("Capture file %s did not appear within " + "timeout" % self.out_path) - self.test.logger.debug("Waiting for packet") - while time.time() < deadline: - self.wait_for_packet_data(deadline) + poll = False + if timeout > 0: + self.test.logger.debug("Waiting for packet") + else: + poll = True + self.test.logger.debug("Polling for packet") + while time.time() < deadline or poll: + if not self.verify_enough_packet_data_in_pcap(): + time.sleep(0) # yield + poll = False + continue p = self._pcap_reader.recv() if p is not None: if filter_out_fn is not None and filter_out_fn(p): @@ -356,8 +366,9 @@ class VppPGInterface(VppInterface): (time.time() - (deadline - timeout))) return p time.sleep(0) # yield + poll = False self.test.logger.debug("Timeout - no packets received") - raise Exception("Packet didn't arrive within timeout") + raise CaptureTimeoutError("Packet didn't arrive within timeout") def create_arp_req(self): """Create ARP request applicable for this interface""" -- cgit 1.2.3-korg From 770e89e6b916319eedd91c6edf16f0d7e89f556c Mon Sep 17 00:00:00 2001 From: Filip Tehlar Date: Tue, 31 Jan 2017 10:39:16 +0100 Subject: Add basic 4o4 LISP unit test Change-Id: I2d812153d7afe7980346382b525af89b3c47e796 Signed-off-by: Filip Tehlar --- test/Makefile | 2 +- test/lisp.py | 326 ++++++++++++++++++++++++++++++++++++++++++++++ test/test_lisp.py | 171 ++++++++++++++++++++++++ test/util.py | 13 ++ test/vpp_papi_provider.py | 135 +++++++++++++++++++ 5 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 test/lisp.py create mode 100644 test/test_lisp.py (limited to 'test/util.py') diff --git a/test/Makefile b/test/Makefile index 65b5a9bd..fd1bc0a4 100644 --- a/test/Makefile +++ b/test/Makefile @@ -12,7 +12,7 @@ UNITTEST_EXTRA_OPTS="-f" endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 +PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 git+https://github.com/klement/py-lispnetworking@setup SCAPY_SOURCE=$(PYTHON_VENV_PATH)/lib/python2.7/site-packages/ BUILD_COV_DIR = $(BR)/test-cov diff --git a/test/lisp.py b/test/lisp.py new file mode 100644 index 00000000..865070df --- /dev/null +++ b/test/lisp.py @@ -0,0 +1,326 @@ +from random import randint +from socket import AF_INET, AF_INET6 +from scapy.all import * +from scapy.packet import * +from scapy.fields import * +from lisp import * +from framework import * +from vpp_object import * + + +class VppLispLocatorSet(VppObject): + """ Represents LISP locator set in VPP """ + + def __init__(self, test, ls_name): + self._test = test + self._ls_name = ls_name + + @property + def test(self): + return self._test + + @property + def ls_name(self): + return self._ls_name + + def add_vpp_config(self): + self.test.vapi.lisp_locator_set(ls_name=self._ls_name) + self._test.registry.register(self, self.test.logger) + + def get_lisp_locator_sets_dump_entry(self): + result = self.test.vapi.lisp_locator_set_dump() + for ls in result: + if ls.ls_name.strip('\x00') == self._ls_name: + return ls + return None + + def query_vpp_config(self): + return self.get_lisp_locator_sets_dump_entry() is not None + + def remove_vpp_config(self): + self.test.vapi.lisp_locator_set(ls_name=self._ls_name, is_add=0) + + def object_id(self): + return 'lisp-locator-set-%s' % self._ls_name + + +class VppLispLocator(VppObject): + """ Represents LISP locator in VPP """ + + def __init__(self, test, sw_if_index, ls_name, priority=1, weight=1): + self._test = test + self._sw_if_index = sw_if_index + self._ls_name = ls_name + self._priority = priority + self._weight = weight + + @property + def test(self): + """ Test which created this locator """ + return self._test + + @property + def ls_name(self): + """ Locator set name """ + return self._ls_name + + @property + def sw_if_index(self): + return self._sw_if_index + + @property + def priority(self): + return self.priority + + @property + def weight(self): + return self._weight + + def add_vpp_config(self): + self.test.vapi.lisp_locator(ls_name=self._ls_name, + sw_if_index=self._sw_if_index, + priority=self._priority, + weight=self._weight) + self._test.registry.register(self, self.test.logger) + + def get_lisp_locator_dump_entry(self): + locators = self.test.vapi.lisp_locator_dump( + is_index_set=0, ls_name=self._ls_name) + for locator in locators: + if locator.sw_if_index == self._sw_if_index: + return locator + return None + + def query_vpp_config(self): + locator = self.get_lisp_locator_dump_entry() + return locator is not None + + def remove_vpp_config(self): + self.test.vapi.lisp_locator( + ls_name=self._ls_name, sw_if_index=self._sw_if_index, + priority=self._priority, weight=self._weight, is_add=0) + self._test.registry.register(self, self.test.logger) + + def object_id(self): + return 'lisp-locator-%s-%d' % (self._ls_name, self._sw_if_index) + + +class LispEIDType(object): + IP4 = 0 + IP6 = 1 + MAC = 2 + + +class LispKeyIdType(object): + NONE = 0 + SHA1 = 1 + SHA256 = 2 + + +class LispEID(object): + """ Lisp endpoint identifier """ + def __init__(self, eid): + self.eid = eid + + # find out whether EID is ip4 prefix, ip6 prefix or MAC + if self.eid.find("/") != -1: + if self.eid.find(":") == -1: + self.eid_type = LispEIDType.IP4 + self.data_length = 4 + else: + self.eid_type = LispEIDType.IP6 + self.data_length = 16 + + self.eid_address = self.eid.split("/")[0] + self.prefix_length = int(self.eid.split("/")[1]) + elif self.eid.count(":") == 5: # MAC address + self.eid_type = LispEIDType.MAC + self.eid_address = self.eid + self.prefix_length = 0 + self.data_length = 6 + else: + raise Exception('Unsupported EID format {}!'.format(eid)) + + def __str__(self): + if self.eid_type == LispEIDType.IP4: + return socket.inet_pton(socket.AF_INET, self.eid_address) + elif self.eid_type == LispEIDType.IP6: + return socket.inet_pton(socket.AF_INET6, self.eid_address) + elif self.eid_type == LispEIDType.MAC: + return Exception('Unimplemented') + raise Exception('Unknown EID type {}!'.format(self.eid_type)) + + +class VppLispMapping(VppObject): + """ Represents common features for remote and local LISP mapping in VPP """ + + def __init__(self, test, eid, vni=0, priority=1, weight=1): + self._eid = LispEID(eid) + self._test = test + self._priority = priority + self._weight = weight + self._vni = vni + + @property + def test(self): + return self._test + + @property + def vni(self): + return self._vni + + @property + def eid(self): + return self._eid + + @property + def priority(self): + return self._priority + + @property + def weight(self): + return self._weight + + def get_lisp_mapping_dump_entry(self): + return self.test.vapi.lisp_eid_table_dump( + eid_set=1, prefix_length=self._eid.prefix_length, + vni=self._vni, eid_type=self._eid.eid_type, eid=str(self._eid)) + + def query_vpp_config(self): + mapping = self.get_lisp_mapping_dump_entry() + return mapping + + +class VppLocalMapping(VppLispMapping): + """ LISP Local mapping """ + def __init__(self, test, eid, ls_name, vni=0, priority=1, weight=1, + key_id=LispKeyIdType.NONE, key=''): + super(VppLocalMapping, self).__init__(test, eid, vni, priority, weight) + self._ls_name = ls_name + self._key_id = key_id + self._key = key + + @property + def ls_name(self): + return self._ls_name + + @property + def key_id(self): + return self._key_id + + @property + def key(self): + return self._key + + def add_vpp_config(self): + self.test.vapi.lisp_local_mapping( + ls_name=self._ls_name, eid_type=self._eid.eid_type, + eid=str(self._eid), prefix_len=self._eid.prefix_length, + vni=self._vni, key_id=self._key_id, key=self._key) + self._test.registry.register(self, self.test.logger) + + def remove_vpp_config(self): + self.test.vapi.lisp_local_mapping( + ls_name=self._ls_name, eid_type=self._eid.eid_type, + eid=str(self._eid), prefix_len=self._eid.prefix_length, + vni=self._vni, is_add=0) + + def object_id(self): + return 'lisp-eid-local-mapping-%s[%d]' % (self._eid, self._vni) + + +class VppRemoteMapping(VppLispMapping): + + def __init__(self, test, eid, rlocs=None, vni=0, priority=1, weight=1): + super(VppRemoteMapping, self).__init__(test, eid, vni, priority, + weight) + self._rlocs = rlocs + + @property + def rlocs(self): + return self._rlocs + + def add_vpp_config(self): + self.test.vapi.lisp_remote_mapping( + rlocs=self._rlocs, eid_type=self._eid.eid_type, + eid=str(self._eid), eid_prefix_len=self._eid.prefix_length, + vni=self._vni, rlocs_num=len(self._rlocs)) + self._test.registry.register(self, self.test.logger) + + def remove_vpp_config(self): + self.test.vapi.lisp_remote_mapping( + eid_type=self._eid.eid_type, eid=str(self._eid), + eid_prefix_len=self._eid.prefix_length, vni=self._vni, + is_add=0, rlocs_num=0) + + def object_id(self): + return 'lisp-eid-remote-mapping-%s[%d]' % (self._eid, self._vni) + + +class VppLispAdjacency(VppObject): + """ Represents LISP adjacency in VPP """ + + def __init__(self, test, leid, reid, vni=0): + self._leid = LispEID(leid) + self._reid = LispEID(reid) + if self._leid.eid_type != self._reid.eid_type: + raise Exception('remote and local EID are different types!') + self._vni = vni + self._test = test + + @property + def test(self): + return self._test + + @property + def leid(self): + return self._leid + + @property + def reid(self): + return self._reid + + @property + def vni(self): + return self._vni + + def add_vpp_config(self): + self.test.vapi.lisp_adjacency( + leid=str(self._leid), + reid=str(self._reid), eid_type=self._leid.eid_type, + leid_len=self._leid.prefix_length, + reid_len=self._reid.prefix_length, vni=self._vni) + self._test.registry.register(self, self.test.logger) + + def eid_equal(self, eid, eid_type, eid_data, prefix_len): + if eid.eid_type != eid_type: + return False + + if eid_type == LispEIDType.IP4 or eid_type == LispEIDType.IP6: + if eid.prefix_length != prefix_len: + return False + + if str(eid) != eid_data[0:eid.data_length]: + return False + + return True + + def query_vpp_config(self): + res = self.test.vapi.lisp_adjacencies_get(vni=self._vni) + for adj in res.adjacencies: + if self.eid_equal(self._leid, adj.eid_type, adj.leid, + adj.leid_prefix_len) and \ + self.eid_equal(self._reid, adj.eid_type, adj.reid, + adj.reid_prefix_len): + return True + return False + + def remove_vpp_config(self): + self.test.vapi.lisp_adjacency( + leid=str(self._leid), + reid=str(self._reid), eid_type=self._leid.eid_type, + leid_len=self._leid.prefix_length, + reid_len=self._reid.prefix_length, vni=self._vni, is_add=0) + + def object_id(self): + return 'lisp-adjacency-%s-%s[%d]' % (self._leid, self._reid, self._vni) diff --git a/test/test_lisp.py b/test/test_lisp.py new file mode 100644 index 00000000..a896698c --- /dev/null +++ b/test/test_lisp.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +import unittest + +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP, Ether +from py_lispnetworking.lisp import LISP_GPE_Header + +from util import ppp, ForeignAddressFactory +from framework import VppTestCase, VppTestRunner +from lisp import * + + +class Driver(object): + + config_order = ['locator-sets', + 'locators', + 'local-mappings', + 'remote-mappings', + 'adjacencies'] + + """ Basic class for data driven testing """ + def __init__(self, test, test_cases): + self._test_cases = test_cases + self._test = test + + @property + def test_cases(self): + return self._test_cases + + @property + def test(self): + return self._test + + def create_packet(self, src_if, dst_if, deid, payload=''): + """ + Create IPv4 packet + + param: src_if + param: dst_if + """ + packet = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=deid) / + Raw(payload)) + return packet + + @abstractmethod + def run(self): + """ testing procedure """ + pass + + +class SimpleDriver(Driver): + """ Implements simple test procedure """ + def __init__(self, test, test_cases): + super(SimpleDriver, self).__init__(test, test_cases) + + def verify_capture(self, src_loc, dst_loc, capture): + """ + Verify captured packet + + :param src_loc: source locator address + :param dst_loc: destination locator address + :param capture: list of captured packets + """ + self.test.assertEqual(len(capture), 1, "Unexpected number of " + "packets! Expected 1 but {} received" + .format(len(capture))) + packet = capture[0] + try: + ip_hdr = packet[IP] + # assert the values match + self.test.assertEqual(ip_hdr.src, src_loc, "IP source address") + self.test.assertEqual(ip_hdr.dst, dst_loc, + "IP destination address") + gpe_hdr = packet[LISP_GPE_Header] + self.test.assertEqual(gpe_hdr.next_proto, 1, + "next_proto is not ipv4!") + ih = gpe_hdr[IP] + self.test.assertEqual(ih.src, self.test.pg0.remote_ip4, + "unexpected source EID!") + self.test.assertEqual(ih.dst, self.test.deid_ip4, + "unexpected dest EID!") + except: + self.test.logger.error(ppp("Unexpected or invalid packet:", + packet)) + raise + + def configure_tc(self, tc): + for config_item in self.config_order: + for vpp_object in tc[config_item]: + vpp_object.add_vpp_config() + + def run(self, dest): + """ Send traffic for each test case and verify that it + is encapsulated """ + for tc in enumerate(self.test_cases): + self.test.logger.info('Running {}'.format(tc[1]['name'])) + self.configure_tc(tc[1]) + + print self.test.vapi.cli("sh lisp loc") + print self.test.vapi.cli("sh lisp eid") + print self.test.vapi.cli("sh lisp adj vni 0") + print self.test.vapi.cli("sh lisp gpe entry") + + packet = self.create_packet(self.test.pg0, self.test.pg1, dest, + 'data') + self.test.pg0.add_stream(packet) + self.test.pg0.enable_capture() + self.test.pg1.enable_capture() + self.test.pg_start() + capture = self.test.pg1.get_capture(1) + self.verify_capture(self.test.pg1.local_ip4, + self.test.pg1.remote_ip4, capture) + self.test.pg0.assert_nothing_captured() + + +class TestLisp(VppTestCase): + """ Basic LISP test """ + + @classmethod + def setUpClass(cls): + super(TestLisp, cls).setUpClass() + cls.faf = ForeignAddressFactory() + cls.create_pg_interfaces(range(2)) # create pg0 and pg1 + for i in cls.pg_interfaces: + i.admin_up() # put the interface upsrc_if + i.config_ip4() # configure IPv4 address on the interface + i.resolve_arp() # resolve ARP, so that we know VPP MAC + + def setUp(self): + super(TestLisp, self).setUp() + self.vapi.lisp_enable_disable(is_enabled=1) + + def test_lisp_basic_encap(self): + """Test case for basic encapsulation""" + + self.deid_ip4_net = self.faf.net + self.deid_ip4 = self.faf.get_ip4() + self.seid_ip4 = '{}/{}'.format(self.pg0.local_ip4, 32) + self.rloc_ip4 = self.pg1.remote_ip4n + + test_cases = [ + { + 'name': 'basic ip4 over ip4', + 'locator-sets': [VppLispLocatorSet(self, 'ls-4o4')], + 'locators': [ + VppLispLocator(self, self.pg1.sw_if_index, 'ls-4o4') + ], + 'local-mappings': [ + VppLocalMapping(self, self.seid_ip4, 'ls-4o4') + ], + 'remote-mappings': [ + VppRemoteMapping(self, self.deid_ip4_net, + [{ + "is_ip4": 1, + "priority": 1, + "weight": 1, + "addr": self.rloc_ip4 + }]) + ], + 'adjacencies': [ + VppLispAdjacency(self, self.seid_ip4, self.deid_ip4_net) + ] + } + ] + self.test_driver = SimpleDriver(self, test_cases) + self.test_driver.run(self.deid_ip4) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index a6484906..3a8bd838 100644 --- a/test/util.py +++ b/test/util.py @@ -100,3 +100,16 @@ class Host(object): self._mac = mac self._ip4 = ip4 self._ip6 = ip6 + + +class ForeignAddressFactory(object): + count = 0 + prefix_len = 24 + net_template = '10.10.10.{}' + net = net_template.format(0) + '/' + str(prefix_len) + + def get_ip4(self): + if self.count > 255: + raise Exception("Network host address exhaustion") + self.count += 1 + return self.net_template.format(self.count) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 842c21b8..32680424 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,4 +1,5 @@ import os +import socket import fnmatch import time from hook import Hook @@ -1301,3 +1302,137 @@ class VppPapiProvider(object): def ip_mfib_dump(self): return self.api(self.papi.ip_mfib_dump, {}) + + def lisp_enable_disable(self, is_enabled): + return self.api( + self.papi.lisp_enable_disable, + { + 'is_en': is_enabled, + }) + + def lisp_locator_set(self, + ls_name, + is_add=1): + return self.api( + self.papi.lisp_add_del_locator_set, + { + 'is_add': is_add, + 'locator_set_name': ls_name + }) + + def lisp_locator_set_dump(self): + return self.api(self.papi.lisp_locator_set_dump, {}) + + def lisp_locator(self, + ls_name, + sw_if_index, + priority=1, + weight=1, + is_add=1): + return self.api( + self.papi.lisp_add_del_locator, + { + 'is_add': is_add, + 'locator_set_name': ls_name, + 'sw_if_index': sw_if_index, + 'priority': priority, + 'weight': weight + }) + + def lisp_locator_dump(self, is_index_set, ls_name=None, ls_index=0): + return self.api( + self.papi.lisp_locator_dump, + { + 'is_index_set': is_index_set, + 'ls_name': ls_name, + 'ls_index': ls_index, + }) + + def lisp_local_mapping(self, + ls_name, + eid_type, + eid, + prefix_len, + vni=0, + key_id=0, + key="", + is_add=1): + return self.api( + self.papi.lisp_add_del_local_eid, + { + 'locator_set_name': ls_name, + 'is_add': is_add, + 'eid_type': eid_type, + 'eid': eid, + 'prefix_len': prefix_len, + 'vni': vni, + 'key_id': key_id, + 'key': key + }) + + def lisp_eid_table_dump(self, + eid_set=0, + prefix_length=0, + vni=0, + eid_type=0, + eid=None, + filter_opt=0): + return self.api( + self.papi.lisp_eid_table_dump, + { + 'eid_set': eid_set, + 'prefix_length': prefix_length, + 'vni': vni, + 'eid_type': eid_type, + 'eid': eid, + 'filter': filter_opt, + }) + + def lisp_remote_mapping(self, + eid_type, + eid, + eid_prefix_len=0, + vni=0, + rlocs=None, + rlocs_num=0, + is_src_dst=0, + is_add=1): + return self.api( + self.papi.lisp_add_del_remote_mapping, + { + 'is_add': is_add, + 'eid_type': eid_type, + 'eid': eid, + 'eid_len': eid_prefix_len, + 'rloc_num': rlocs_num, + 'rlocs': rlocs, + 'vni': vni, + 'is_src_dst': is_src_dst, + }) + + def lisp_adjacency(self, + leid, + reid, + leid_len, + reid_len, + eid_type, + is_add=1, + vni=0): + return self.api( + self.papi.lisp_add_del_adjacency, + { + 'is_add': is_add, + 'vni': vni, + 'eid_type': eid_type, + 'leid': leid, + 'reid': reid, + 'leid_len': leid_len, + 'reid_len': reid_len, + }) + + def lisp_adjacencies_get(self, vni=0): + return self.api( + self.papi.lisp_adjacencies_get, + { + 'vni': vni + }) -- cgit 1.2.3-korg From 39f9d8bd226ab5aa366f181a5cbf7c873f599e06 Mon Sep 17 00:00:00 2001 From: Neale Ranns Date: Thu, 16 Feb 2017 21:57:05 -0800 Subject: [Proxy] ARP tests Change-Id: I40d6d763b55a26cdee0afef85d1acdd19dd10dd6 Signed-off-by: Neale Ranns --- test/test_neighbor.py | 425 ++++++++++++++++++++++++++++++++++++++++++++++ test/util.py | 5 + test/vpp_interface.py | 13 ++ test/vpp_neighbor.py | 77 +++++++++ test/vpp_papi_provider.py | 67 ++++++++ 5 files changed, 587 insertions(+) create mode 100644 test/test_neighbor.py create mode 100644 test/vpp_neighbor.py (limited to 'test/util.py') diff --git a/test/test_neighbor.py b/test/test_neighbor.py new file mode 100644 index 00000000..6a608091 --- /dev/null +++ b/test/test_neighbor.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python + +import unittest +from socket import AF_INET, AF_INET6, inet_pton + +from framework import VppTestCase, VppTestRunner +from vpp_neighbor import VppNeighbor, find_nbr + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet import IP, UDP + +# not exported by scapy, so redefined here +arp_opts = {"who-has": 1, "is-at": 2} + + +class ARPTestCase(VppTestCase): + """ ARP Test Case """ + + def setUp(self): + super(ARPTestCase, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(4)) + + # pg0 configured with ip4 and 6 addresses used for input + # pg1 configured with ip4 and 6 addresses used for output + # pg2 is unnumbered to pg0 + for i in self.pg_interfaces: + i.admin_up() + + self.pg0.config_ip4() + self.pg0.config_ip6() + self.pg0.resolve_arp() + + self.pg1.config_ip4() + self.pg1.config_ip6() + + # pg3 in a different VRF + self.pg3.set_table_ip4(1) + self.pg3.config_ip4() + + def verify_arp_req(self, rx, smac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") + self.assertEqual(ether.src, smac) + + arp = rx[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + self.assertEqual(arp.op, arp_opts["who-has"]) + self.assertEqual(arp.hwsrc, smac) + self.assertEqual(arp.hwdst, "00:00:00:00:00:00") + self.assertEqual(arp.psrc, sip) + self.assertEqual(arp.pdst, dip) + + def verify_arp_resp(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + arp = rx[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + self.assertEqual(arp.op, arp_opts["is-at"]) + self.assertEqual(arp.hwsrc, smac) + self.assertEqual(arp.hwdst, dmac) + self.assertEqual(arp.psrc, sip) + self.assertEqual(arp.pdst, dip) + + def verify_ip(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + ip = rx[IP] + self.assertEqual(ip.src, sip) + self.assertEqual(ip.dst, dip) + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.assert_nothing_captured(remark=remark) + + def test_arp(self): + """ ARP """ + + # + # Generate some hosts on the LAN + # + self.pg1.generate_remote_hosts(4) + + # + # Send IP traffic to one of these unresolved hosts. + # expect the generation of an ARP request + # + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1._remote_hosts[1].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_arp_req(rx[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # And a dynamic ARP entry for host 1 + # + dyn_arp = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip4) + dyn_arp.add_vpp_config() + + # + # now we expect IP traffic forwarded + # + dyn_p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[1].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(dyn_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # And a Static ARP entry for host 2 + # + static_arp = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[2].mac, + self.pg1.remote_hosts[2].ip4, + is_static=1) + static_arp.add_vpp_config() + + static_p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[2].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(static_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[2].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[2].ip4) + + # + # flap the link. dynamic ARPs get flush, statics don't + # + self.pg1.admin_down() + self.pg1.admin_up() + + self.pg0.add_stream(static_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[2].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[2].ip4) + + self.pg0.add_stream(dyn_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_req(rx[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # Send an ARP request from one of the so-far unlearned remote hosts + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg1._remote_hosts[3].mac) / + ARP(op="who-has", + hwsrc=self.pg1._remote_hosts[3].mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1._remote_hosts[3].ip4)) + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1._remote_hosts[3].mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[3].ip4) + + # + # VPP should have learned the mapping for the remote host + # + self.assertTrue(find_nbr(self, + self.pg1.sw_if_index, + self.pg1._remote_hosts[3].ip4)) + + # + # ERROR Cases + # 1 - don't respond to ARP request for address not within the + # interface's sub-net + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst="10.10.10.3", + psrc=self.pg0.remote_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local destination") + + # + # 2 - don't respond to ARP request from an address not within the + # interface's sub-net + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc="10.10.10.3", + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + + # + # 3 - don't respond to ARP request from an address that belongs to + # the router + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc=self.pg0.local_ip4, + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + + # + # 4 - don't respond to ARP requests that has mac source different + # from ARP request HW source + # the router + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc="00:00:00:DE:AD:BE", + psrc=self.pg0.remote_ip4, + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + + # + # cleanup + # + dyn_arp.remove_vpp_config() + static_arp.remove_vpp_config() + + def test_proxy_arp(self): + """ Proxy ARP """ + + # + # Proxy ARP rewquest packets for each interface + # + arp_req_pg2 = (Ether(src=self.pg2.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst="10.10.10.3", + psrc=self.pg1.remote_ip4)) + arp_req_pg0 = (Ether(src=self.pg0.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst="10.10.10.3", + psrc=self.pg0.remote_ip4)) + arp_req_pg1 = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.3", + psrc=self.pg1.remote_ip4)) + arp_req_pg3 = (Ether(src=self.pg3.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg3.remote_mac, + pdst="10.10.10.3", + psrc=self.pg3.remote_ip4)) + + # + # Configure Proxy ARP for 10.10.10.0 -> 10.10.10.124 + # + self.vapi.proxy_arp_add_del(inet_pton(AF_INET, "10.10.10.2"), + inet_pton(AF_INET, "10.10.10.124")) + + # + # No responses are sent when the interfaces are not enabled for proxy + # ARP + # + self.send_and_assert_no_replies(self.pg0, arp_req_pg0, + "ARP req from unconfigured interface") + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from unconfigured interface") + + # + # Make pg2 un-numbered to pg1 + # still won't reply. + # + self.pg2.set_unnumbered(self.pg1.sw_if_index) + + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from unnumbered interface") + + # + # Enable each interface to reply to proxy ARPs + # + for i in self.pg_interfaces: + i.set_proxy_arp() + + # + # Now each of the interfaces should reply to a request to a proxied + # address + # + self.pg0.add_stream(arp_req_pg0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg0.local_mac, + self.pg0.remote_mac, + "10.10.10.3", + self.pg0.remote_ip4) + + self.pg1.add_stream(arp_req_pg1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1.remote_mac, + "10.10.10.3", + self.pg1.remote_ip4) + + self.pg2.add_stream(arp_req_pg2) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + "10.10.10.3", + self.pg1.remote_ip4) + + # + # A request for an address out of the configured range + # + arp_req_pg1_hi = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.125", + psrc=self.pg1.remote_ip4)) + self.send_and_assert_no_replies(self.pg1, arp_req_pg1_hi, + "ARP req out of range HI") + arp_req_pg1_low = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.1", + psrc=self.pg1.remote_ip4)) + self.send_and_assert_no_replies(self.pg1, arp_req_pg1_low, + "ARP req out of range Low") + + # + # Request for an address in the proxy range but from an interface + # in a different VRF + # + self.send_and_assert_no_replies(self.pg3, arp_req_pg3, + "ARP req from different VRF") + + # + # Disable Each interface for proxy ARP + # - expect none to respond + # + for i in self.pg_interfaces: + i.set_proxy_arp(0) + + self.send_and_assert_no_replies(self.pg0, arp_req_pg0, + "ARP req from disable") + self.send_and_assert_no_replies(self.pg1, arp_req_pg1, + "ARP req from disable") + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from disable") diff --git a/test/util.py b/test/util.py index 3a8bd838..d6b77f9d 100644 --- a/test/util.py +++ b/test/util.py @@ -47,6 +47,11 @@ def ip4n_range(ip4n, s, e): for ip in ip4_range(ip4, s, e)) +def mactobinary(mac): + """ Convert the : separated format into binary packet data for the API """ + return mac.replace(':', '').decode('hex') + + class NumericConstant(object): __metaclass__ = ABCMeta diff --git a/test/vpp_interface.py b/test/vpp_interface.py index 9c904aac..125d8f04 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -2,6 +2,7 @@ from abc import abstractmethod, ABCMeta import socket from util import Host +from vpp_neighbor import VppNeighbor class VppInterface(object): @@ -316,3 +317,15 @@ class VppInterface(object): i.table_id == self.ip4_table_id: return True return False + + def set_unnumbered(self, ip_sw_if_index): + """ Set the interface to unnumbered via ip_sw_if_index """ + self.test.vapi.sw_interface_set_unnumbered( + self.sw_if_index, + ip_sw_if_index) + + def set_proxy_arp(self, enable=1): + """ Set the interface to enable/disable Proxy ARP """ + self.test.vapi.proxy_arp_intfc_enable_disable( + self.sw_if_index, + enable) diff --git a/test/vpp_neighbor.py b/test/vpp_neighbor.py new file mode 100644 index 00000000..fbd41eb5 --- /dev/null +++ b/test/vpp_neighbor.py @@ -0,0 +1,77 @@ +""" + Neighbour Entries + + object abstractions for ARP and ND +""" + +from socket import inet_pton, inet_ntop, AF_INET, AF_INET6 +from vpp_object import * +from util import mactobinary + + +def find_nbr(test, sw_if_index, ip_addr, is_static=0, inet=AF_INET): + nbrs = test.vapi.ip_neighbor_dump(sw_if_index, + is_ipv6=1 if AF_INET6 == inet else 0) + if inet == AF_INET: + s = 4 + else: + s = 16 + nbr_addr = inet_pton(inet, ip_addr) + + for n in nbrs: + if nbr_addr == n.ip_address[:s] \ + and is_static == n.is_static: + return True + return False + + +class VppNeighbor(VppObject): + """ + ARP Entry + """ + + def __init__(self, test, sw_if_index, mac_addr, nbr_addr, + af=AF_INET, is_static=0): + self._test = test + self.sw_if_index = sw_if_index + self.mac_addr = mactobinary(mac_addr) + self.af = af + self.is_static = is_static + self.nbr_addr = inet_pton(af, nbr_addr) + + def add_vpp_config(self): + self._test.vapi.ip_neighbor_add_del( + self.sw_if_index, + self.mac_addr, + self.nbr_addr, + is_add=1, + is_ipv6=1 if AF_INET6 == self.af else 0, + is_static=self.is_static) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + self._test.vapi.ip_neighbor_add_del( + self.sw_if_index, + self.mac_addr, + self.nbr_addr, + is_ipv6=1 if AF_INET6 == self.af else 0, + is_add=0, + is_static=self.is_static) + + def query_vpp_config(self): + dump = self._test.vapi.ip_neighbor_dump( + self.sw_if_index, + is_ipv6=1 if AF_INET6 == self.af else 0) + for n in dump: + if self.nbr_addr == n.ip_address \ + and self.is_static == n.is_static: + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d:%s" + % (self.sw_if_index, + inet_ntop(self.af, self.nbr_addr))) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index dd9baff1..67b3e141 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -240,6 +240,20 @@ class VppPapiProvider(object): 'address_length': addr_len, 'address': addr}) + def sw_interface_set_unnumbered(self, sw_if_index, ip_sw_if_index, + is_add=1): + """ Set the Interface to be unnumbered + + :param is_add: (Default value = 1) + :param sw_if_index - interface That will be unnumbered + :param ip_sw_if_index - interface with an IP addres + + """ + return self.api(self.papi.sw_interface_set_unnumbered, + {'sw_if_index': ip_sw_if_index, + 'unnumbered_sw_if_index': sw_if_index, + 'is_add': is_add}) + def sw_interface_enable_disable_mpls(self, sw_if_index, is_enable=1): """ @@ -638,6 +652,59 @@ class VppPapiProvider(object): } ) + def ip_neighbor_dump(self, + sw_if_index, + is_ipv6=0): + """ Return IP neighbor dump. + + :param sw_if_index: + :param int is_ipv6: 1 for IPv6 neighbor, 0 for IPv4. (Default = 0) + """ + + return self.api( + self.papi.ip_neighbor_dump, + {'is_ipv6': is_ipv6, + 'sw_if_index': sw_if_index + } + ) + + def proxy_arp_add_del(self, + low_address, + hi_address, + vrf_id=0, + is_add=1): + """ Config Proxy Arp Range. + + :param low_address: Start address in the rnage to Proxy for + :param hi_address: End address in the rnage to Proxy for + :param vrf_id: The VRF/table in which to proxy + """ + + return self.api( + self.papi.proxy_arp_add_del, + {'vrf_id': vrf_id, + 'is_add': is_add, + 'low_address': low_address, + 'hi_address': hi_address, + } + ) + + def proxy_arp_intfc_enable_disable(self, + sw_if_index, + is_enable=1): + """ Enable/Disable an interface for proxy ARP requests + + :param sw_if_index: Interface + :param enable_disable: Enable/Disable + """ + + return self.api( + self.papi.proxy_arp_intfc_enable_disable, + {'sw_if_index': sw_if_index, + 'enable_disable': is_enable + } + ) + def reset_vrf(self, vrf_id, is_ipv6=0, -- cgit 1.2.3-korg From 2a3ea49d5cc224ffb2cf02bacaf0c02ddae12b86 Mon Sep 17 00:00:00 2001 From: Neale Ranns Date: Wed, 19 Apr 2017 05:24:40 -0700 Subject: Learn IP6 link-local ND entries from NSs sourced from link-local address Change-Id: I4c3ce4d58df7977490fc94991291422ea1e31ee3 Signed-off-by: Neale Ranns --- src/vnet/ethernet/arp.c | 3 ++ src/vnet/ip/ip6_neighbor.c | 7 ++- test/test_dhcp.py | 8 +--- test/test_ip6.py | 106 ++++++++++++++++++++++++++++++++++----------- test/util.py | 21 ++++++++- test/vpp_interface.py | 18 +++++++- test/vpp_neighbor.py | 2 +- 7 files changed, 126 insertions(+), 39 deletions(-) (limited to 'test/util.py') diff --git a/src/vnet/ethernet/arp.c b/src/vnet/ethernet/arp.c index 3e292e4d..31af4fba 100644 --- a/src/vnet/ethernet/arp.c +++ b/src/vnet/ethernet/arp.c @@ -584,6 +584,9 @@ vnet_arp_set_ip4_over_ethernet_internal (vnet_main_t * vnm, FIB_PROTOCOL_IP4, &pfx.fp_addr, e->sw_if_index, ~0, 1, NULL, FIB_ROUTE_PATH_FLAG_NONE); + } + else + { e->flags |= ETHERNET_ARP_IP4_ENTRY_FLAG_NO_FIB_ENTRY; } } diff --git a/src/vnet/ip/ip6_neighbor.c b/src/vnet/ip/ip6_neighbor.c index 42edb79a..31182770 100644 --- a/src/vnet/ip/ip6_neighbor.c +++ b/src/vnet/ip/ip6_neighbor.c @@ -634,6 +634,9 @@ vnet_set_ip6_ethernet_neighbor (vlib_main_t * vm, FIB_PROTOCOL_IP6, &pfx.fp_addr, n->key.sw_if_index, ~0, 1, NULL, FIB_ROUTE_PATH_FLAG_NONE); + } + else + { n->flags |= IP6_NEIGHBOR_FLAG_NO_FIB_ENTRY; } } @@ -1027,7 +1030,7 @@ icmp6_neighbor_solicitation_or_advertisement (vlib_main_t * vm, /* If src address unspecified or link local, donot learn neighbor MAC */ if (PREDICT_TRUE (error0 == ICMP6_ERROR_NONE && o0 != 0 && - !ip6_sadd_unspecified && !ip6_sadd_link_local)) + !ip6_sadd_unspecified)) { ip6_neighbor_main_t *nm = &ip6_neighbor_main; if (nm->limit_neighbor_cache_size && @@ -1040,7 +1043,7 @@ icmp6_neighbor_solicitation_or_advertisement (vlib_main_t * vm, &h0->target_address, o0->ethernet_address, sizeof (o0->ethernet_address), - 0, 0); + 0, ip6_sadd_link_local); } if (is_solicitation && error0 == ICMP6_ERROR_NONE) diff --git a/test/test_dhcp.py b/test/test_dhcp.py index 89667d3d..03c749d3 100644 --- a/test/test_dhcp.py +++ b/test/test_dhcp.py @@ -6,6 +6,7 @@ import struct from framework import VppTestCase, VppTestRunner from vpp_neighbor import VppNeighbor +from util import mk_ll_addr from scapy.layers.l2 import Ether, getmacbyip from scapy.layers.inet import IP, UDP, ICMP @@ -24,13 +25,6 @@ DHCP6_CLIENT_PORT = 547 DHCP6_SERVER_PORT = 546 -def mk_ll_addr(mac): - - euid = in6_mactoifaceid(mac) - addr = "fe80::" + euid - return addr - - class TestDHCP(VppTestCase): """ DHCP Test Case """ diff --git a/test/test_ip6.py b/test/test_ip6.py index a8e8d4de..3ba09230 100644 --- a/test/test_ip6.py +++ b/test/test_ip6.py @@ -77,7 +77,6 @@ class TestIPv6ND(VppTestCase): def send_and_expect_ra(self, intf, pkts, remark, dst_ip=None, filter_out_fn=is_ipv6_misc): intf.add_stream(pkts) - self.pg0.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() rx = intf.get_capture(1, filter_out_fn=filter_out_fn) @@ -86,11 +85,25 @@ class TestIPv6ND(VppTestCase): rx = rx[0] self.validate_ra(intf, rx, dst_ip) + def send_and_expect_na(self, intf, pkts, remark, dst_ip=None, + tgt_ip=None, + filter_out_fn=is_ipv6_misc): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = intf.get_capture(1, filter_out_fn=filter_out_fn) + + self.assertEqual(len(rx), 1) + rx = rx[0] + self.validate_na(intf, rx, dst_ip, tgt_ip) + def send_and_assert_no_replies(self, intf, pkts, remark): intf.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() - intf.assert_nothing_captured(remark=remark) + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) class TestIPv6(TestIPv6ND): @@ -376,6 +389,59 @@ class TestIPv6(TestIPv6ND): 128, inet=AF_INET6)) + # + # send an NS from a link local address to the interface's global + # address + # + p = (Ether(dst=in6_getnsmac(nsma), src=self.pg0.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[2].ip6_ll) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + + self.send_and_expect_na(self.pg0, p, + "NS from link-local", + dst_ip=self.pg0._remote_hosts[2].ip6_ll, + tgt_ip=self.pg0.local_ip6) + + # + # we should have learned an ND entry for the peer's link-local + # but not inserted a route to it in the FIB + # + self.assertTrue(find_nbr(self, + self.pg0.sw_if_index, + self.pg0._remote_hosts[2].ip6_ll, + inet=AF_INET6)) + self.assertFalse(find_route(self, + self.pg0._remote_hosts[2].ip6_ll, + 128, + inet=AF_INET6)) + + # + # An NS to the router's own Link-local + # + p = (Ether(dst=in6_getnsmac(nsma), src=self.pg0.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[3].ip6_ll) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6_ll) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + + self.send_and_expect_na(self.pg0, p, + "NS to/from link-local", + dst_ip=self.pg0._remote_hosts[3].ip6_ll, + tgt_ip=self.pg0.local_ip6_ll) + + # + # we should have learned an ND entry for the peer's link-local + # but not inserted a route to it in the FIB + # + self.assertTrue(find_nbr(self, + self.pg0.sw_if_index, + self.pg0._remote_hosts[3].ip6_ll, + inet=AF_INET6)) + self.assertFalse(find_route(self, + self.pg0._remote_hosts[3].ip6_ll, + 128, + inet=AF_INET6)) + def validate_ra(self, intf, rx, dst_ip=None, mtu=9000, pi_opt=None): if not dst_ip: dst_ip = intf.remote_ip6 @@ -770,14 +836,10 @@ class IPv6NDProxyTest(TestIPv6ND): # # try that NS again. this time we expect an NA back # - self.pg1.add_stream(ns_pg1) - self.pg_enable_capture(self.pg_interfaces) - self.pg_start() - rx = self.pg1.get_capture(1) - - self.validate_na(self.pg1, rx[0], - dst_ip=self.pg0._remote_hosts[2].ip6, - tgt_ip=self.pg0.local_ip6) + self.send_and_expect_na(self.pg1, ns_pg1, + "NS to proxy entry", + dst_ip=self.pg0._remote_hosts[2].ip6, + tgt_ip=self.pg0.local_ip6) # # ... and that we have an entry in the ND cache @@ -816,14 +878,10 @@ class IPv6NDProxyTest(TestIPv6ND): ICMPv6ND_NS(tgt=self.pg0._remote_hosts[2].ip6) / ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) - self.pg0.add_stream(ns_pg0) - self.pg_enable_capture(self.pg_interfaces) - self.pg_start() - rx = self.pg0.get_capture(1) - - self.validate_na(self.pg0, rx[0], - tgt_ip=self.pg0._remote_hosts[2].ip6, - dst_ip=self.pg0.remote_ip6) + self.send_and_expect_na(self.pg0, ns_pg0, + "NS to proxy entry on main", + tgt_ip=self.pg0._remote_hosts[2].ip6, + dst_ip=self.pg0.remote_ip6) # # Setup and resolve proxy for another host on another interface @@ -837,14 +895,10 @@ class IPv6NDProxyTest(TestIPv6ND): inet_pton(AF_INET6, self.pg0._remote_hosts[3].ip6), self.pg2.sw_if_index) - self.pg2.add_stream(ns_pg2) - self.pg_enable_capture(self.pg_interfaces) - self.pg_start() - rx = self.pg2.get_capture(1) - - self.validate_na(self.pg2, rx[0], - dst_ip=self.pg0._remote_hosts[3].ip6, - tgt_ip=self.pg0.local_ip6) + self.send_and_expect_na(self.pg2, ns_pg2, + "NS to proxy entry other interface", + dst_ip=self.pg0._remote_hosts[3].ip6, + tgt_ip=self.pg0.local_ip6) self.assertTrue(find_nbr(self, self.pg2.sw_if_index, diff --git a/test/util.py b/test/util.py index d6b77f9d..aeba2ab4 100644 --- a/test/util.py +++ b/test/util.py @@ -4,6 +4,7 @@ import socket import sys from abc import abstractmethod, ABCMeta from cStringIO import StringIO +from scapy.layers.inet6 import in6_mactoifaceid def ppp(headline, packet): @@ -52,6 +53,12 @@ def mactobinary(mac): return mac.replace(':', '').decode('hex') +def mk_ll_addr(mac): + euid = in6_mactoifaceid(mac) + addr = "fe80::" + euid + return addr + + class NumericConstant(object): __metaclass__ = ABCMeta @@ -101,10 +108,22 @@ class Host(object): """ IPv6 address of remote host - raw, suitable as API parameter.""" return socket.inet_pton(socket.AF_INET6, self._ip6) - def __init__(self, mac=None, ip4=None, ip6=None): + @property + def ip6_ll(self): + """ IPv6 link-local address - string """ + return self._ip6_ll + + @property + def ip6n_ll(self): + """ IPv6 link-local address of remote host - + raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET6, self._ip6_ll) + + def __init__(self, mac=None, ip4=None, ip6=None, ip6_ll=None): self._mac = mac self._ip4 = ip4 self._ip6 = ip6 + self._ip6_ll = ip6_ll class ForeignAddressFactory(object): diff --git a/test/vpp_interface.py b/test/vpp_interface.py index 5dba0978..662015ea 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -1,7 +1,7 @@ from abc import abstractmethod, ABCMeta import socket -from util import Host +from util import Host, mk_ll_addr from vpp_neighbor import VppNeighbor @@ -54,6 +54,16 @@ class VppInterface(object): """Local IPv6 address - raw, suitable as API parameter.""" return socket.inet_pton(socket.AF_INET6, self.local_ip6) + @property + def local_ip6_ll(self): + """Local IPv6 linnk-local address on VPP interface (string).""" + return self._local_ip6_ll + + @property + def local_ip6n_ll(self): + """Local IPv6 link-local address - raw, suitable as API parameter.""" + return self.local_ip6n_ll + @property def remote_ip6(self): """IPv6 address of remote peer "connected" to this interface.""" @@ -133,7 +143,8 @@ class VppInterface(object): mac = "02:%02x:00:00:ff:%02x" % (self.sw_if_index, i) ip4 = "172.16.%u.%u" % (self.sw_if_index, i) ip6 = "fd01:%x::%x" % (self.sw_if_index, i) - host = Host(mac, ip4, ip6) + ip6_ll = mk_ll_addr(mac) + host = Host(mac, ip4, ip6, ip6_ll) self._remote_hosts.append(host) self._hosts_by_mac[mac] = host self._hosts_by_ip4[ip4] = host @@ -176,6 +187,9 @@ class VppInterface(object): "Could not find interface with sw_if_index %d " "in interface dump %s" % (self.sw_if_index, repr(r))) + self._local_ip6_ll = mk_ll_addr(self.local_mac) + self._local_ip6n_ll = socket.inet_pton(socket.AF_INET6, + self.local_ip6_ll) def config_ip4(self): """Configure IPv4 address on the VPP interface.""" diff --git a/test/vpp_neighbor.py b/test/vpp_neighbor.py index 6968b5f6..5919cf8e 100644 --- a/test/vpp_neighbor.py +++ b/test/vpp_neighbor.py @@ -31,7 +31,7 @@ class VppNeighbor(VppObject): """ def __init__(self, test, sw_if_index, mac_addr, nbr_addr, - af=AF_INET, is_static=False, is_no_fib_entry=False): + af=AF_INET, is_static=False, is_no_fib_entry=0): self._test = test self.sw_if_index = sw_if_index self.mac_addr = mactobinary(mac_addr) -- cgit 1.2.3-korg From c86e592f9a65e5098c9a70c38ee228252dbd32ce Mon Sep 17 00:00:00 2001 From: Eyal Bari Date: Sun, 2 Jul 2017 18:33:16 +0300 Subject: TEST:add L2BD arp term tests Change-Id: I42414da9663ecfc8dfe5baf3e6615cf3b9b02e22 Signed-off-by: Eyal Bari --- test/test_l2bd_arp_term.py | 239 +++++++++++++++++++++++++++++++++++++++++++++ test/util.py | 26 +++++ test/vpp_papi_provider.py | 8 ++ 3 files changed, 273 insertions(+) create mode 100644 test/test_l2bd_arp_term.py (limited to 'test/util.py') diff --git a/test/test_l2bd_arp_term.py b/test/test_l2bd_arp_term.py new file mode 100644 index 00000000..52c1ad32 --- /dev/null +++ b/test/test_l2bd_arp_term.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +""" L2BD ARP term Test """ + +import unittest +import random +import copy + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet import IP + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp, mactobinary + + +class TestL2bdArpTerm(VppTestCase): + """ L2BD arp termination Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestL2bdArpTerm, cls).setUpClass() + + try: + # Create pg interfaces + n_bd = 1 + cls.ifs_per_bd = ifs_per_bd = 3 + n_ifs = n_bd * ifs_per_bd + cls.create_pg_interfaces(range(n_ifs)) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + cls.hosts = set() + + except Exception: + super(TestL2bdArpTerm, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + self.reset_packet_infos() + super(TestL2bdArpTerm, self).setUp() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2bdArpTerm, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show bridge-domain 1 detail")) + + def add_del_arp_term_hosts(self, entries, bd_id=1, is_add=1): + for e in entries: + self.vapi.bd_ip_mac_add_del(bd_id=bd_id, + mac=e.bin_mac, + ip=e.ip4n, + is_ipv6=0, + is_add=is_add) + + @classmethod + def mac_list(cls, b6_range): + return ["00:00:ca:fe:00:%02x" % b6 for b6 in b6_range] + + @classmethod + def ip4_host(cls, subnet, host, mac): + return Host(mac=mac, + ip4="172.17.1%02u.%u" % (subnet, host)) + + @classmethod + def ip4_hosts(cls, subnet, start, mac_list): + return {cls.ip4_host(subnet, start + j, mac_list[j]) + for j in range(len(mac_list))} + + @classmethod + def bd_swifs(cls, b): + n = cls.ifs_per_bd + start = (b - 1) * n + return [cls.pg_interfaces[j] for j in range(start, start + n)] + + def bd_add_del(self, bd_id=1, is_add=1): + if is_add: + self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add) + for swif in self.bd_swifs(bd_id): + swif_idx = swif.sw_if_index + self.vapi.sw_interface_set_l2_bridge( + swif_idx, bd_id=bd_id, enable=is_add) + if not is_add: + self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add) + + @classmethod + def arp(cls, src_host, host): + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=src_host.mac) / + ARP(op="who-has", + hwsrc=src_host.bin_mac, + pdst=host.ip4, + psrc=src_host.ip4)) + + @classmethod + def arp_reqs(cls, src_host, entries): + return [cls.arp(src_host, e) for e in entries] + + def response_host(self, src_host, arp_resp): + ether = arp_resp[Ether] + self.assertEqual(ether.dst, src_host.mac) + + arp = arp_resp[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + arp_opts = {"who-has": 1, "is-at": 2} + self.assertEqual(arp.op, arp_opts["is-at"]) + self.assertEqual(arp.hwdst, src_host.mac) + self.assertEqual(arp.pdst, src_host.ip4) + return Host(arp.hwsrc, arp.psrc) + + def arp_resp_hosts(self, src_host, pkts): + return {self.response_host(src_host, p) for p in pkts} + + def set_bd_flags(self, bd_id, **args): + """ + Enable/disable defined feature(s) of the bridge domain. + + :param int bd_id: Bridge domain ID. + :param list args: List of feature/status pairs. Allowed features: \ + learn, forward, flood, uu_flood and arp_term. Status False means \ + disable, status True means enable the feature. + :raise: ValueError in case of unknown feature in the input. + """ + for flag in args: + if flag == "learn": + feature_bitmap = 1 << 0 + elif flag == "forward": + feature_bitmap = 1 << 1 + elif flag == "flood": + feature_bitmap = 1 << 2 + elif flag == "uu_flood": + feature_bitmap = 1 << 3 + elif flag == "arp_term": + feature_bitmap = 1 << 4 + else: + raise ValueError("Unknown feature used: %s" % flag) + is_set = 1 if args[flag] else 0 + self.vapi.bridge_flags(bd_id, is_set, feature_bitmap) + self.logger.info("Bridge domain ID %d updated" % bd_id) + + def verify_arp(self, src_host, req_hosts, resp_hosts, bd_id=1): + reqs = self.arp_reqs(src_host, req_hosts) + + for swif in self.bd_swifs(bd_id): + swif.add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + for swif in self.bd_swifs(bd_id): + resp_pkts = swif.get_capture(len(resp_hosts)) + resps = self.arp_resp_hosts(src_host, resp_pkts) + self.assertEqual(len(resps ^ resp_hosts), 0) + + def test_l2bd_arp_term_01(self): + """ L2BD arp term - add 5 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(1, 5)) + hosts = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(hosts, is_add=1) + self.verify_arp(src_host, hosts, hosts) + type(self).hosts = hosts + + def test_l2bd_arp_term_02(self): + """ L2BD arp term - delete 3 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(1, 3)) + deleted = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(deleted, is_add=0) + remaining = self.hosts - deleted + self.verify_arp(src_host, self.hosts, remaining) + type(self).hosts = remaining + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_03(self): + """ L2BD arp term - recreate BD1, readd 3 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(1, 3)) + readded = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(readded, is_add=1) + self.verify_arp(src_host, self.hosts | readded, readded) + type(self).hosts = readded + + def test_l2bd_arp_term_04(self): + """ L2BD arp term - 2 IP4 addrs per host + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(1, 3)) + sub5_hosts = self.ip4_hosts(5, 1, macs) + self.add_del_arp_term_hosts(sub5_hosts, is_add=1) + hosts = self.hosts | sub5_hosts + self.verify_arp(src_host, hosts, hosts) + type(self).hosts = hosts + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_05(self): + """ L2BD arp term - create and update 10 IP4-mac pairs + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs1 = self.mac_list(range(10, 20)) + hosts1 = self.ip4_hosts(5, 1, macs1) + self.add_del_arp_term_hosts(hosts1, is_add=1) + self.verify_arp(src_host, hosts1, hosts1) + macs2 = self.mac_list(range(20, 30)) + hosts2 = self.ip4_hosts(5, 1, macs2) + self.add_del_arp_term_hosts(hosts2, is_add=1) + self.verify_arp(src_host, hosts1, hosts2) + self.bd_add_del(1, is_add=0) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index aeba2ab4..d6aa9a42 100644 --- a/test/util.py +++ b/test/util.py @@ -88,6 +88,11 @@ class Host(object): """ MAC address """ return self._mac + @property + def bin_mac(self): + """ MAC address """ + return mactobinary(self._mac) + @property def ip4(self): """ IPv4 address - string """ @@ -119,6 +124,27 @@ class Host(object): raw, suitable as API parameter.""" return socket.inet_pton(socket.AF_INET6, self._ip6_ll) + def __eq__(self, other): + if isinstance(other, Host): + return (self.mac == other.mac and + self.ip4 == other.ip4 and + self.ip6 == other.ip6 and + self.ip6_ll == other.ip6_ll) + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "Host { mac:%s ip4:%s ip6:%s ip6_ll:%s }" % (self.mac, + self.ip4, + self.ip6, + self.ip6_ll) + + def __hash__(self): + return hash(self.__repr__()) + def __init__(self, mac=None, ip4=None, ip6=None, ip6_ll=None): self._mac = mac self._ip4 = ip4 diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 4f7e9fee..51c359e8 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -411,6 +411,14 @@ class VppPapiProvider(object): 'arp_term': arp_term, 'is_add': is_add}) + def bd_ip_mac_add_del(self, bd_id, mac, ip, is_ipv6=0, is_add=1): + return self.api(self.papi.bd_ip_mac_add_del, + {'bd_id': bd_id, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'ip_address': ip, + 'mac_address': mac}) + def l2fib_add_del(self, mac, bd_id, sw_if_index, is_add=1, static_mac=0, filter_mac=0, bvi_mac=0): """Create/delete L2 FIB entry. -- cgit 1.2.3-korg From 92dc12a01b1f5c75500bb015bd159d6bba0547b5 Mon Sep 17 00:00:00 2001 From: Andrew Yourtchenko Date: Thu, 7 Sep 2017 13:22:24 +0200 Subject: test: factor out "L4_Conn" into a class within util.py (VPP-931) It seems a useful abstraction for the purposes of writing fine-grained tests, to be able to create a "connection" object which would be bound to two VPP interfaces, and hold some information about the state, allowing to send the packets back and forth with minimal amount of arguments. Change-Id: Idb83b6b82b38bded5b7e1756a41bb2df4cd58e3a Signed-off-by: Andrew Yourtchenko --- test/test_acl_plugin_conns.py | 52 ++-------------------------------- test/util.py | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 50 deletions(-) (limited to 'test/util.py') diff --git a/test/test_acl_plugin_conns.py b/test/test_acl_plugin_conns.py index 06f3cf7e..0d4aa09d 100644 --- a/test/test_acl_plugin_conns.py +++ b/test/test_acl_plugin_conns.py @@ -13,6 +13,7 @@ from scapy.layers.inet6 import ICMPv6EchoReply, IPv6ExtHdrRouting from scapy.layers.inet6 import IPv6ExtHdrFragment from pprint import pprint from random import randint +from util import L4_Conn def to_acl_rule(self, is_permit, wildcard_sport=False): @@ -68,37 +69,7 @@ class IterateWithSleep(): self.testcase.sleep(self.sleep_sec) -class Conn(): - def __init__(self, testcase, if1, if2, af, l4proto, port1, port2): - self.testcase = testcase - self.ifs = [None, None] - self.ifs[0] = if1 - self.ifs[1] = if2 - self.address_family = af - self.l4proto = l4proto - self.ports = [None, None] - self.ports[0] = port1 - self.ports[1] = port2 - self - - def pkt(self, side, flags=None): - is_ip6 = 1 if self.address_family == AF_INET6 else 0 - s0 = side - s1 = 1-side - src_if = self.ifs[s0] - dst_if = self.ifs[s1] - layer_3 = [IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4), - IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)] - payload = "x" - l4args = {'sport': self.ports[s0], 'dport': self.ports[s1]} - if flags is not None: - l4args['flags'] = flags - p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / - layer_3[is_ip6] / - self.l4proto(**l4args) / - Raw(payload)) - return p - +class Conn(L4_Conn): def apply_acls(self, reflect_side, acl_side): pkts = [] pkts.append(self.pkt(0)) @@ -152,25 +123,6 @@ class Conn(): } return new_rule - def send(self, side, flags=None): - self.ifs[side].add_stream(self.pkt(side, flags)) - self.ifs[1-side].enable_capture() - self.testcase.pg_start() - - def recv(self, side): - p = self.ifs[side].wait_for_packet(1) - return p - - def send_through(self, side, flags=None): - self.send(side, flags) - p = self.recv(1-side) - return p - - def send_pingpong(self, side, flags1=None, flags2=None): - p1 = self.send_through(side, flags1) - p2 = self.send_through(1-side, flags2) - return [p1, p2] - @unittest.skipUnless(running_extended_tests(), "part of extended tests") class ACLPluginConnTestCase(VppTestCase): diff --git a/test/util.py b/test/util.py index d6aa9a42..3e0267a3 100644 --- a/test/util.py +++ b/test/util.py @@ -6,6 +6,13 @@ from abc import abstractmethod, ABCMeta from cStringIO import StringIO from scapy.layers.inet6 import in6_mactoifaceid +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP, TCP +from scapy.layers.inet6 import IPv6, ICMPv6Unknown, ICMPv6EchoRequest +from scapy.packet import Packet +from socket import inet_pton, AF_INET, AF_INET6 + def ppp(headline, packet): """ Return string containing the output of scapy packet.show() call. """ @@ -163,3 +170,62 @@ class ForeignAddressFactory(object): raise Exception("Network host address exhaustion") self.count += 1 return self.net_template.format(self.count) + + +class L4_Conn(): + """ L4 'connection' tied to two VPP interfaces """ + def __init__(self, testcase, if1, if2, af, l4proto, port1, port2): + self.testcase = testcase + self.ifs = [None, None] + self.ifs[0] = if1 + self.ifs[1] = if2 + self.address_family = af + self.l4proto = l4proto + self.ports = [None, None] + self.ports[0] = port1 + self.ports[1] = port2 + self + + def pkt(self, side, l4args={}, payload="x"): + is_ip6 = 1 if self.address_family == AF_INET6 else 0 + s0 = side + s1 = 1-side + src_if = self.ifs[s0] + dst_if = self.ifs[s1] + layer_3 = [IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4), + IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)] + merged_l4args = {'sport': self.ports[s0], 'dport': self.ports[s1]} + merged_l4args.update(l4args) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + layer_3[is_ip6] / + self.l4proto(**merged_l4args) / + Raw(payload)) + return p + + def send(self, side, flags=None, payload=""): + l4args = {} + if flags is not None: + l4args['flags'] = flags + self.ifs[side].add_stream(self.pkt(side, + l4args=l4args, payload=payload)) + self.ifs[1-side].enable_capture() + self.testcase.pg_start() + + def recv(self, side): + p = self.ifs[side].wait_for_packet(1) + return p + + def send_through(self, side, flags=None, payload=""): + self.send(side, flags, payload) + p = self.recv(1-side) + return p + + def send_pingpong(self, side, flags1=None, flags2=None): + p1 = self.send_through(side, flags1) + p2 = self.send_through(1-side, flags2) + return [p1, p2] + + +class L4_CONN_SIDE: + L4_CONN_SIDE_ZERO = 0 + L4_CONN_SIDE_ONE = 1 -- cgit 1.2.3-korg