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/Makefile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/Makefile (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 00000000..7cbcf97a --- /dev/null +++ b/test/Makefile @@ -0,0 +1,3 @@ + +all: + @python run_tests.py discover -p test_$(TEST)"*.py" -- 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/Makefile') 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 277b89c946e6fdc764ee48726fcd3df1c189eda9 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Fri, 28 Oct 2016 13:20:27 +0200 Subject: add vpp debugging support to test framework improve test documentation Change-Id: Ia9678aa2532ecb4cb33736aedb4a31aa3f2a3f93 Signed-off-by: Klement Sekera --- .gitignore | 2 + Makefile | 27 ++++-- test/Makefile | 50 +++++++++-- test/doc/Makefile | 188 +++++++++++++++++++++------------------- test/doc/conf.py | 1 + test/framework.py | 214 ++++++++++++++++++++++++++++++++-------------- test/hook.py | 112 ++++++++++++++++++++++-- test/log.py | 71 +++++++++++++++ test/test_l2bd.py | 2 +- test/test_l2xc.py | 12 ++- test/vpp_papi_provider.py | 19 +++- 11 files changed, 510 insertions(+), 188 deletions(-) create mode 100644 test/log.py (limited to 'test/Makefile') diff --git a/.gitignore b/.gitignore index e88652cd..126780bb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ /build-root/*.deb /build-root/*.rpm /build-root/*.changes +/build-root/test-doc/ +/build-roit/python/ /build-config.mk /dpdk/*.tar.gz /dpdk/*.tar.xz diff --git a/Makefile b/Makefile index 54b0f29d..3a6c7834 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ endif DEB_DEPENDS = curl build-essential autoconf automake bison libssl-dev ccache DEB_DEPENDS += debhelper dkms git libtool libganglia1-dev libapr1-dev dh-systemd DEB_DEPENDS += libconfuse-dev git-review exuberant-ctags cscope -DEB_DEPENDS += python-dev +DEB_DEPENDS += python-dev python-virtualenv ifeq ($(OS_VERSION_ID),14.04) DEB_DEPENDS += openjdk-8-jdk-headless else @@ -58,7 +58,7 @@ endif .PHONY: help bootstrap wipe wipe-release build build-release rebuild rebuild-release .PHONY: run run-release debug debug-release build-vat run-vat pkg-deb pkg-rpm .PHONY: ctags cscope plugins plugins-release build-vpp-api -.PHONY: test test-debug retest retest-debug +.PHONY: test test-debug retest retest-debug test-doc test-wipe-doc test-help test-wipe help: @echo "Make Targets:" @@ -78,8 +78,10 @@ help: @echo " debug-release - run release binary with debugger" @echo " test - build and run functional tests" @echo " test-debug - build and run functional tests (debug build)" + @echo " test-wipe - wipe files generated by unit tests" @echo " retest - run functional tests" @echo " retest-debug - run functional tests (debug build)" + @echo " test-help - show help on test framework" @echo " build-vat - build vpp-api-test tool" @echo " build-vpp-api - build vpp-api" @echo " run-vat - run vpp-api-test tool" @@ -93,6 +95,8 @@ help: @echo " doxygen - (re)generate documentation" @echo " bootstrap-doxygen - setup Doxygen dependencies" @echo " wipe-doxygen - wipe all generated documentation" + @echo " test-doc - generate documentation for test framework" + @echo " test-wipe-doc - wipe documentation for test framework" @echo "" @echo "Make Arguments:" @echo " V=[0|1] - set build verbosity level" @@ -105,7 +109,7 @@ help: @echo " PLATFORM= - target platform. default is vpp" @echo " TEST= - only run specific test" @echo "" - @echo "Current Argumernt Values:" + @echo "Current Argument Values:" @echo " V = $(V)" @echo " STARTUP_CONF = $(STARTUP_CONF)" @echo " STARTUP_DIR = $(STARTUP_DIR)" @@ -188,7 +192,7 @@ plugins-release: $(BR)/.bootstrap.ok build-vpp-api: $(BR)/.bootstrap.ok $(call make,$(PLATFORM)_debug,vpp-api-install) -PYTHON_PATH=$(BR)/python +VPP_PYTHON_PREFIX=$(BR)/python define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install vpp-api-test-install,) @@ -197,7 +201,7 @@ define test 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) + WS_ROOT=$(WS_ROOT) V=$(V) TEST=$(TEST) VPP_PYTHON_PREFIX=$(VPP_PYTHON_PREFIX) $(3) endef test: bootstrap @@ -206,12 +210,17 @@ test: bootstrap test-debug: bootstrap $(call test,vpp_lite,vpp_lite_debug,test) +test-help: + @make -C test help + +test-wipe: + @make -C test wipe + 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 + @make -C test WS_ROOT=$(WS_ROOT) BR=$(BR) VPP_PYTHON_PREFIX=$(VPP_PYTHON_PREFIX) doc -test-clean: - make -C test clean +test-wipe-doc: + @make -C test wipe-doc BR=$(BR) retest: $(call test,vpp_lite,vpp_lite,retest) diff --git a/test/Makefile b/test/Makefile index c90679e8..63cee203 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,22 +1,58 @@ -PYTHON_VENV_PATH=$(PYTHON_PATH)/virtualenv +.PHONY: verify-python-path -test: clean +verify-python-path: +ifndef VPP_PYTHON_PREFIX + $(error VPP_PYTHON_PREFIX is not set) +endif + +PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv + +test: wipe verify-python-path @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 +retest: wipe verify-python-path @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" -.PHONY: clean doc +.PHONY: wipe doc -clean: +wipe: verify-python-path @rm -f /dev/shm/vpp-unittest-* @rm -rf /tmp/vpp-unittest-* -doc: +doc: verify-python-path @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" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" + +wipe-doc: + @make -C doc wipe BR=$(BR) + +help: + @echo "Running tests:" + @echo "" + @echo " test - build and run functional tests" + @echo " test-debug - build and run functional tests (debug build)" + @echo " retest - run functional tests" + @echo " retest-debug - run functional tests (debug build)" + @echo " wipe-test - wipe (temporary) files generated by unit tests" + @echo "" + @echo "Arguments controlling test runs:" + @echo " V=[0|1|2] - set test verbosity level" + @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 " + @echo " and waiting for user input before running " + @echo " and tearing down a testcase" + @echo " DEBUG=gdbserver - run gdb inside a gdb server, otherwise " + @echo " same as above" + @echo " STEP=[yes|no] - ease debugging by stepping through a testcase " + @echo " TEST= - only run specific test" + @echo "" + @echo "Creating test documentation" + @echo " test-doc - generate documentation for test framework" + @echo " wipe-test-doc - wipe documentation for test framework" + @echo "" diff --git a/test/doc/Makefile b/test/doc/Makefile index 00655389..809abef8 100644 --- a/test/doc/Makefile +++ b/test/doc/Makefile @@ -3,20 +3,34 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SRC_DOC_DIR = $(WS_ROOT)/test/doc +SPHINXBUILD = sphinx-build PAPER = -BUILDDIR = _build +BUILD_DOC_ROOT = $(BR)/test-doc +BUILD_DOC_DIR = $(BUILD_DOC_ROOT)/build +API_DOC_GEN_DIR = $(BUILD_DOC_ROOT)/apidoc # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -d $(BUILD_DOC_DIR)/.sphinx-cache $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(API_DOC_GEN_DIR) -c $(SRC_DOC_DIR) # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +INDEX_REL_PATH:=$(shell realpath --relative-to=$(API_DOC_GEN_DIR) $(SRC_DOC_DIR)/index.rst) +IN_VENV:=$(shell if pip -V | grep "virtualenv" 2>&1 > /dev/null; then echo 1; else echo 0; fi) + +.PHONY: verify-virtualenv +verify-virtualenv: +ifeq ($(IN_VENV),0) + $(error "Not running inside virtualenv (are you running 'make test-doc' from root?)") +endif .PHONY: regen-api-doc -regen-api-doc: - sphinx-apidoc -o . .. +regen-api-doc: verify-virtualenv + @mkdir -p $(API_DOC_GEN_DIR) + #@echo ".. include:: $(INDEX_REL_PATH)" > $(API_DOC_GEN_DIR)/index.rst + @cp $(SRC_DOC_DIR)/index.rst $(API_DOC_GEN_DIR) + sphinx-apidoc -o $(API_DOC_GEN_DIR) .. .PHONY: help help: @@ -48,182 +62,182 @@ help: @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: wipe +wipe: + rm -rf $(BUILD_DOC_ROOT) .PHONY: html -html: regen-api-doc - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html +html: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/html @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/html." .PHONY: dirhtml -dirhtml: regen-api-doc - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml +dirhtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dirhtml @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/dirhtml." .PHONY: singlehtml -singlehtml: regen-api-doc - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml +singlehtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/singlehtml @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + @echo "Build finished. The HTML page is in $(BUILD_DOC_DIR)/singlehtml." .PHONY: pickle -pickle: regen-api-doc - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle +pickle: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json -json: regen-api-doc - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json +json: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp -htmlhelp: regen-api-doc - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp +htmlhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." + ".hhp project file in $(BUILD_DOC_DIR)/htmlhelp." .PHONY: qthelp -qthelp: regen-api-doc - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp +qthelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/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" + ".qhcp project file in $(BUILD_DOC_DIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VPPtestframework.qhc" + @echo "# assistant -collectionFile $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhc" .PHONY: applehelp -applehelp: regen-api-doc - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp +applehelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/applehelp @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "Build finished. The help book is in $(BUILD_DOC_DIR)/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 +devhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/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 "# ln -s $(BUILD_DOC_DIR)/devhelp $$HOME/.local/share/devhelp/VPPtestframework" @echo "# devhelp" .PHONY: epub -epub: regen-api-doc - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub +epub: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + @echo "Build finished. The epub file is in $(BUILD_DOC_DIR)/epub." .PHONY: epub3 epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub3 @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + @echo "Build finished. The epub3 file is in $(BUILD_DOC_DIR)/epub3." .PHONY: latex -latex: regen-api-doc - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +latex: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Build finished; the LaTeX files are in $(BUILD_DOC_DIR)/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 +latexpdf: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: latexpdfja -latexpdfja: regen-api-doc - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +latexpdfja: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/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." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: text -text: regen-api-doc - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text +text: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/text @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." + @echo "Build finished. The text files are in $(BUILD_DOC_DIR)/text." .PHONY: man -man: regen-api-doc - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man +man: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/man @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + @echo "Build finished. The manual pages are in $(BUILD_DOC_DIR)/man." .PHONY: texinfo -texinfo: regen-api-doc - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo +texinfo: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Build finished. The Texinfo files are in $(BUILD_DOC_DIR)/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 +info: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + make -C $(BUILD_DOC_DIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILD_DOC_DIR)/texinfo." .PHONY: gettext -gettext: regen-api-doc - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale +gettext: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILD_DOC_DIR)/locale @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + @echo "Build finished. The message catalogs are in $(BUILD_DOC_DIR)/locale." .PHONY: changes -changes: regen-api-doc - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes +changes: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/changes @echo - @echo "The overview file is in $(BUILDDIR)/changes." + @echo "The overview file is in $(BUILD_DOC_DIR)/changes." .PHONY: linkcheck -linkcheck: regen-api-doc - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck +linkcheck: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." + "or in $(BUILD_DOC_DIR)/linkcheck/output.txt." .PHONY: doctest -doctest: regen-api-doc - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest +doctest: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." + "results in $(BUILD_DOC_DIR)/doctest/output.txt." .PHONY: coverage -coverage: regen-api-doc - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage +coverage: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + "results in $(BUILD_DOC_DIR)/coverage/python.txt." .PHONY: xml -xml: regen-api-doc - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml +xml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/xml @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + @echo "Build finished. The XML files are in $(BUILD_DOC_DIR)/xml." .PHONY: pseudoxml -pseudoxml: regen-api-doc - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml +pseudoxml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pseudoxml @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + @echo "Build finished. The pseudo-XML files are in $(BUILD_DOC_DIR)/pseudoxml." .PHONY: dummy -dummy: regen-api-doc - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy +dummy: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." diff --git a/test/doc/conf.py b/test/doc/conf.py index aa841714..cec9ddee 100644 --- a/test/doc/conf.py +++ b/test/doc/conf.py @@ -20,6 +20,7 @@ import os import sys sys.path.insert(0, os.path.abspath('..')) + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. diff --git a/test/framework.py b/test/framework.py index 02ffb7ad..5f75e010 100644 --- a/test/framework.py +++ b/test/framework.py @@ -2,20 +2,20 @@ from abc import * import os -import sys import subprocess import unittest import tempfile +import time import resource from time import sleep +from Queue import Queue +from threading import Thread from inspect import getdoc -from hook import PollHook +from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface from vpp_papi_provider import VppPapiProvider - from scapy.packet import Raw - -from logging import * +from log import * """ Test framework module. @@ -24,41 +24,6 @@ from logging import * 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 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' - COLOR_RESET = '\033[0m' -else: - RED = '' - GREEN = '' - YELLOW = '' - LPURPLE = '' - COLOR_RESET = '' - - -""" @var formatting delimiter consisting of '=' characters """ -double_line_delim = '=' * 70 -""" @var formatting delimiter consisting of '-' characters """ -single_line_delim = '-' * 70 - class _PacketInfo(object): """Private class to create packet info object. @@ -84,6 +49,11 @@ class _PacketInfo(object): data = None +def pump_output(out, queue): + for line in iter(out.readline, b''): + queue.put(line) + + class VppTestCase(unittest.TestCase): """ Subclass of the python unittest.TestCase class. @@ -107,24 +77,86 @@ class VppTestCase(unittest.TestCase): """Return the instance of this testcase""" return cls.test_instance + @classmethod + def set_debug_flags(cls, d): + cls.debug_core = False + cls.debug_gdb = False + cls.debug_gdbserver = False + if d is None: + return + dl = d.lower() + if dl == "core": + if resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0: + # give a heads up if this is actually useless + cls.logger.critical("WARNING: core size limit is set 0, core " + "files will NOT be created") + cls.debug_core = True + elif dl == "gdb": + cls.debug_gdb = True + elif dl == "gdbserver": + cls.debug_gdbserver = True + else: + raise Exception("Unrecognized DEBUG option: '%s'" % d) + @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 + s = os.getenv("STEP") + cls.step = True if s.lower() in ("y", "yes", "1") 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.step = False + try: + d = os.getenv("DEBUG") + except: + d = None + cls.set_debug_flags(d) cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') 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.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline) + + @classmethod + def wait_for_enter(cls): + if cls.debug_gdbserver: + print(double_line_delim) + print("Spawned GDB server with PID: %d" % cls.vpp.pid) + elif cls.debug_gdb: + print(double_line_delim) + print("Spawned VPP with PID: %d" % cls.vpp.pid) + else: + cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid) + return + print(single_line_delim) + print("You can debug the VPP using e.g.:") + if cls.debug_gdbserver: + print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'") + print("Now is the time to attach a gdb by running the above " + "command, set up breakpoints etc. and then resume VPP from " + "within gdb by issuing the 'continue' command") + elif cls.debug_gdb: + print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid) + print("Now is the time to attach a gdb by running the above " + "command and set up breakpoints etc.") + print(single_line_delim) + raw_input("Press ENTER to continue running the testcase...") + + @classmethod + def run_vpp(cls): + cmdline = cls.vpp_cmdline + + if cls.debug_gdbserver: + cmdline = ['gdbserver', 'localhost:7777'] + cls.vpp_cmdline + cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline)) + + cls.vpp = subprocess.Popen(cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1) + cls.wait_for_enter() @classmethod def setUpClass(cls): @@ -132,37 +164,66 @@ class VppTestCase(unittest.TestCase): Perform class setup before running the testcase Remove shared memory files, start vpp and connect the vpp-api """ + cls.logger = getLogger(cls.__name__) 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.logger.info("Temporary dir is %s, shm prefix is %s", + cls.tempdir, cls.shm_prefix) cls.setUpConstants() cls.pg_streams = [] cls.packet_infos = {} cls.verbose = 0 print(double_line_delim) - print(YELLOW + getdoc(cls) + COLOR_RESET) + print(colorize(getdoc(cls), YELLOW)) 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.run_vpp() cls.vpp_dead = False cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix) - cls.vapi.register_hook(PollHook(cls)) - cls.vapi.connect() + if cls.step: + cls.vapi.register_hook(StepHook(cls)) + else: + cls.vapi.register_hook(PollHook(cls)) + time.sleep(0.1) + try: + cls.vapi.connect() + except: + if cls.debug_gdbserver: + print(colorize("You're running VPP inside gdbserver but " + "VPP-API connection failed, did you forget " + "to 'continue' VPP from within gdb?", RED)) + raise + cls.vpp_stdout_queue = Queue() + cls.vpp_stdout_reader_thread = Thread( + target=pump_output, args=(cls.vpp.stdout, cls.vpp_stdout_queue)) + cls.vpp_stdout_reader_thread.start() + cls.vpp_stderr_queue = Queue() + cls.vpp_stderr_reader_thread = Thread( + target=pump_output, args=(cls.vpp.stderr, cls.vpp_stderr_queue)) + cls.vpp_stderr_reader_thread.start() except: cls.vpp.terminate() del cls.vpp + raise @classmethod def quit(cls): """ Disconnect vpp-api, kill vpp and cleanup shared memory files """ + if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'): + cls.vpp.poll() + if cls.vpp.returncode is None: + print(double_line_delim) + print("VPP or GDB server is still running") + print(single_line_delim) + raw_input("When done debugging, press ENTER to kill the process" + " and finish running the testcase...") + if hasattr(cls, 'vpp'): cls.vapi.disconnect() cls.vpp.poll() @@ -170,6 +231,29 @@ class VppTestCase(unittest.TestCase): cls.vpp.terminate() del cls.vpp + if hasattr(cls, 'vpp_stdout_queue'): + cls.logger.info(single_line_delim) + cls.logger.info('VPP output to stdout while running %s:', + cls.__name__) + cls.logger.info(single_line_delim) + f = open(cls.tempdir + '/vpp_stdout.txt', 'w') + while not cls.vpp_stdout_queue.empty(): + line = cls.vpp_stdout_queue.get_nowait() + f.write(line) + cls.logger.info('VPP stdout: %s' % line.rstrip('\n')) + + if hasattr(cls, 'vpp_stderr_queue'): + cls.logger.info(single_line_delim) + cls.logger.info('VPP output to stderr while running %s:', + cls.__name__) + cls.logger.info(single_line_delim) + f = open(cls.tempdir + '/vpp_stderr.txt', 'w') + while not cls.vpp_stderr_queue.empty(): + line = cls.vpp_stderr_queue.get_nowait() + f.write(line) + cls.logger.info('VPP stderr: %s' % line.rstrip('\n')) + cls.logger.info(single_line_delim) + @classmethod def tearDownClass(cls): """ Perform final cleanup after running all tests in this test-case """ @@ -178,11 +262,11 @@ class VppTestCase(unittest.TestCase): def tearDown(self): """ 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")) + self.logger.info(self.vapi.cli("show int")) + self.logger.info(self.vapi.cli("show trace")) + self.logger.info(self.vapi.cli("show hardware")) + self.logger.info(self.vapi.cli("show error")) + self.logger.info(self.vapi.cli("show run")) def setUp(self): """ Clear trace before running each test""" @@ -392,7 +476,7 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addSuccess(self, test) - self.result_string = GREEN + "OK" + COLOR_RESET + self.result_string = colorize("OK", GREEN) def addSkip(self, test, reason): """ @@ -403,7 +487,7 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addSkip(self, test, reason) - self.result_string = YELLOW + "SKIP" + COLOR_RESET + self.result_string = colorize("SKIP", YELLOW) def addFailure(self, test, err): """ @@ -415,10 +499,10 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addFailure(self, test, err) if hasattr(test, 'tempdir'): - self.result_string = RED + "FAIL" + COLOR_RESET + \ + self.result_string = colorize("FAIL", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' else: - self.result_string = RED + "FAIL" + COLOR_RESET + ' [no temp dir]' + self.result_string = colorize("FAIL", RED) + ' [no temp dir]' def addError(self, test, err): """ @@ -430,10 +514,10 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addError(self, test, err) if hasattr(test, 'tempdir'): - self.result_string = RED + "ERROR" + COLOR_RESET + \ + self.result_string = colorize("ERROR", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' else: - self.result_string = RED + "ERROR" + COLOR_RESET + ' [no temp dir]' + self.result_string = colorize("ERROR", RED) + ' [no temp dir]' def getDescription(self, test): """ diff --git a/test/hook.py b/test/hook.py index 9489aa9d..3ae14737 100644 --- a/test/hook.py +++ b/test/hook.py @@ -1,7 +1,8 @@ import signal import os import pexpect -from logging import * +import traceback +from log import * class Hook(object): @@ -9,6 +10,9 @@ class Hook(object): Generic hooks before/after API/CLI calls """ + def __init__(self, logger): + self.logger = logger + def before_api(self, api_name, api_args): """ Function called before API call @@ -17,7 +21,8 @@ class Hook(object): @param api_name: name of the API @param api_args: tuple containing the API arguments """ - debug("API: %s (%s)" % (api_name, api_args)) + self.logger.debug("API: %s (%s)" % + (api_name, api_args), extra={'color': RED}) def after_api(self, api_name, api_args): """ @@ -35,7 +40,7 @@ class Hook(object): @param cli: CLI string """ - debug("CLI: %s" % (cli)) + self.logger.debug("CLI: %s" % (cli), extra={'color': RED}) def after_cli(self, cli): """ @@ -54,6 +59,7 @@ class PollHook(Hook): def __init__(self, testcase): self.vpp_dead = False self.testcase = testcase + self.logger = testcase.logger def spawn_gdb(self, gdb_path, core_path): gdb_cmdline = gdb_path + ' ' + self.testcase.vpp_bin + ' ' + core_path @@ -74,11 +80,12 @@ class PollHook(Hook): self.spawn_gdb(gdb_path, core_path) return else: - error("Debugger '%s' does not exist or is not an executable.." % - gdb_path) + self.logger.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) + self.logger.critical('core file present, debug with: gdb ' + + self.testcase.vpp_bin + ' ' + core_path) def poll_vpp(self): """ @@ -97,7 +104,7 @@ class PollHook(Hook): msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( self.testcase.vpp.returncode, signaldict[abs(self.testcase.vpp.returncode)]) - critical(msg) + self.logger.critical(msg) core_path = self.testcase.tempdir + '/core' if os.path.isfile(core_path): self.on_crash(core_path) @@ -126,3 +133,92 @@ class PollHook(Hook): """ super(PollHook, self).after_cli(cli) self.poll_vpp() + + +class StepHook(PollHook): + """ Hook which requires user to press ENTER before doing any API/CLI """ + + def __init__(self, testcase): + self.skip_stack = None + self.skip_num = None + self.skip_count = 0 + super(StepHook, self).__init__(testcase) + + def skip(self): + if self.skip_stack is None: + return False + stack = traceback.extract_stack() + counter = 0 + skip = True + for e in stack: + if counter > self.skip_num: + break + if e[0] != self.skip_stack[counter][0]: + skip = False + if e[1] != self.skip_stack[counter][1]: + skip = False + counter += 1 + if skip: + self.skip_count += 1 + return True + else: + print("%d API/CLI calls skipped in specified stack " + "frame" % self.skip_count) + self.skip_count = 0 + self.skip_stack = None + self.skip_num = None + return False + + def user_input(self): + print('number\tfunction\tfile\tcode') + counter = 0 + stack = traceback.extract_stack() + for e in stack: + print('%02d.\t%s\t%s:%d\t[%s]' % (counter, e[2], e[0], e[1], e[3])) + counter += 1 + print(single_line_delim) + print("You can enter a number of stack frame chosen from above") + print("Calls in/below that stack frame will be not be stepped anymore") + print(single_line_delim) + while True: + choice = raw_input("Enter your choice, if any, and press ENTER to " + "continue running the testcase...") + if choice == "": + choice = None + try: + if choice is not None: + num = int(choice) + except: + print("Invalid input") + continue + if choice is not None and (num < 0 or num >= len(stack)): + print("Invalid choice") + continue + break + if choice is not None: + self.skip_stack = stack + self.skip_num = num + + def before_cli(self, cli): + """ Wait for ENTER before executing CLI """ + if self.skip(): + print("Skip pause before executing CLI: %s" % cli) + else: + print(double_line_delim) + print("Test paused before executing CLI: %s" % cli) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_cli(cli) + + def before_api(self, api_name, api_args): + """ Wait for ENTER before executing API """ + if self.skip(): + print("Skip pause before executing API: %s (%s)" + % (api_name, api_args)) + else: + print(double_line_delim) + print("Test paused before executing API: %s (%s)" + % (api_name, api_args)) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_api(api_name, api_args) diff --git a/test/log.py b/test/log.py new file mode 100644 index 00000000..38610b7e --- /dev/null +++ b/test/log.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +import sys +import os +import logging + +""" @var formatting delimiter consisting of '=' characters """ +double_line_delim = '=' * 70 +""" @var formatting delimiter consisting of '-' characters """ +single_line_delim = '-' * 70 + + +def colorize(msg, color): + return color + msg + COLOR_RESET + + +class ColorFormatter(logging.Formatter): + + def init(self, fmt=None, datefmt=None): + super(ColorFormatter, self).__init__(fmt, datefmt) + + def format(self, record): + message = super(ColorFormatter, self).format(record) + if hasattr(record, 'color'): + message = colorize(message, record.color) + return message + +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(ColorFormatter()) + +global_logger = logging.getLogger() +global_logger.addHandler(handler) +try: + verbose = int(os.getenv("V", 0)) +except: + verbose = 0 + +# 40 = ERROR, 30 = WARNING, 20 = INFO, 10 = DEBUG, 0 = NOTSET (all messages) +if verbose >= 2: + log_level = 10 +elif verbose == 1: + log_level = 20 +else: + log_level = 40 + +scapy_logger = logging.getLogger("scapy.runtime") +scapy_logger.setLevel(logging.ERROR) + + +def getLogger(name): + logger = logging.getLogger(name) + logger.setLevel(log_level) + return logger + +# 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 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' + COLOR_RESET = '\033[0m' +else: + RED = '' + GREEN = '' + YELLOW = '' + LPURPLE = '' + COLOR_RESET = '' diff --git a/test/test_l2bd.py b/test/test_l2bd.py index 0fced5d9..53e1ee05 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -189,7 +189,7 @@ class TestL2bd(VppTestCase): MAC learning enabled learn 100 MAC enries 3 interfaces: untagged, dot1q, dot1ad (dot1q used instead of dot1ad - in the first version) + in the first version) 2.sending l2 eth pkts between 3 interface 64B, 512B, 1518B, 9200B (ether_size) diff --git a/test/test_l2xc.py b/test/test_l2xc.py index d448a04d..35c7a818 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -78,7 +78,6 @@ class TestL2xc(VppTestCase): 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] = [] @@ -151,12 +150,11 @@ class TestL2xc(VppTestCase): """ L2XC test Test scenario: - 1.config - 2 pairs of 2 interfaces, l2xconnected - - 2.sending l2 eth packets between 4 interfaces - 64B, 512B, 1518B, 9018B (ether_size) - burst of packets per interface + 1. config + 2 pairs of 2 interfaces, l2xconnected + 2. sending l2 eth packets between 4 interfaces + 64B, 512B, 1518B, 9018B (ether_size) + burst of packets per interface """ # Create incoming packet streams for packet-generator interfaces diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 454f3edc..261a0f4a 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,7 +1,19 @@ -import vpp_papi +import os from logging import error from hook import Hook +do_import = True +try: + no_vpp_papi = os.getenv("NO_VPP_PAPI") + if no_vpp_papi == "1": + do_import = False +except: + pass + +if do_import: + import vpp_papi + + # from vnet/vnet/mpls/mpls_types.h MPLS_IETF_MAX_LABEL = 0xfffff MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 @@ -16,7 +28,7 @@ class VppPapiProvider(object): """ def __init__(self, name, shm_prefix): - self.hook = Hook() + self.hook = Hook("vpp-papi-provider") self.name = name self.shm_prefix = shm_prefix @@ -130,7 +142,6 @@ class VppPapiProvider(object): default_router, max_interval, min_interval, lifetime, initial_count, initial_interval, async)) - def vxlan_add_del_tunnel( self, src_addr, @@ -177,7 +188,7 @@ class VppPapiProvider(object): :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. + if equal to 0. :type rx_sw_if_index: str or int :type rx_sw_if_index: str or int :type enable: int -- cgit 1.2.3-korg From 778c2765c8ea5c6628f6d668847f0b9ae06dbf3d Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 8 Nov 2016 02:00:28 +0100 Subject: Improve test framework documentation Change-Id: I06f0cbbbdd29e04a07f1db6807b3e16f1d41e8d2 Signed-off-by: Klement Sekera --- Makefile | 2 +- test/Makefile | 8 +-- test/doc/Makefile | 61 +++++++--------- test/doc/conf.py | 6 +- test/doc/framework.rst | 7 ++ test/doc/hook.rst | 7 ++ test/doc/index.rst | 157 +++++++++++++++++++++++++++++++++++++++-- test/doc/log.rst | 7 ++ test/doc/modules.rst | 24 +++++++ test/doc/run_tests.rst | 7 ++ test/doc/scapy_handlers.rst | 22 ++++++ test/doc/template_bd.rst | 7 ++ test/doc/test_ip.rst | 7 ++ test/doc/test_ip6.rst | 7 ++ test/doc/test_l2bd.rst | 7 ++ test/doc/test_l2xc.rst | 7 ++ test/doc/test_lb.rst | 7 ++ test/doc/test_mpls.rst | 7 ++ test/doc/test_vxlan.rst | 7 ++ test/doc/util.rst | 7 ++ test/doc/vpp_interface.rst | 7 ++ test/doc/vpp_papi_provider.rst | 7 ++ test/doc/vpp_pg_interface.rst | 7 ++ test/doc/vpp_sub_interface.rst | 7 ++ test/test_infra.md | 145 ------------------------------------- test/vpp_pg_interface.py | 41 +++++++++-- 26 files changed, 386 insertions(+), 199 deletions(-) create mode 100644 test/doc/framework.rst create mode 100644 test/doc/hook.rst create mode 100644 test/doc/log.rst create mode 100644 test/doc/modules.rst create mode 100644 test/doc/run_tests.rst create mode 100644 test/doc/scapy_handlers.rst create mode 100644 test/doc/template_bd.rst create mode 100644 test/doc/test_ip.rst create mode 100644 test/doc/test_ip6.rst create mode 100644 test/doc/test_l2bd.rst create mode 100644 test/doc/test_l2xc.rst create mode 100644 test/doc/test_lb.rst create mode 100644 test/doc/test_mpls.rst create mode 100644 test/doc/test_vxlan.rst create mode 100644 test/doc/util.rst create mode 100644 test/doc/vpp_interface.rst create mode 100644 test/doc/vpp_papi_provider.rst create mode 100644 test/doc/vpp_pg_interface.rst create mode 100644 test/doc/vpp_sub_interface.rst delete mode 100644 test/test_infra.md (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 98700864..5064686c 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ endif DEB_DEPENDS = curl build-essential autoconf automake bison libssl-dev ccache DEB_DEPENDS += debhelper dkms git libtool libganglia1-dev libapr1-dev dh-systemd DEB_DEPENDS += libconfuse-dev git-review exuberant-ctags cscope -DEB_DEPENDS += python-dev python-virtualenv +DEB_DEPENDS += python-dev python-virtualenv python-pip ifeq ($(OS_VERSION_ID),14.04) DEB_DEPENDS += openjdk-8-jdk-headless else diff --git a/test/Makefile b/test/Makefile index 63cee203..aac637d3 100644 --- a/test/Makefile +++ b/test/Makefile @@ -6,11 +6,11 @@ ifndef VPP_PYTHON_PREFIX endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv +PYTHON_DEPENDS=scapy pexpect test: wipe verify-python-path @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 && pip install $(PYTHON_DEPENDS)" @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\"" @@ -19,13 +19,13 @@ retest: wipe verify-python-path .PHONY: wipe doc -wipe: verify-python-path +wipe: @rm -f /dev/shm/vpp-unittest-* @rm -rf /tmp/vpp-unittest-* doc: verify-python-path @virtualenv $(PYTHON_VENV_PATH) - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" wipe-doc: diff --git a/test/doc/Makefile b/test/doc/Makefile index 809abef8..3b4c02b8 100644 --- a/test/doc/Makefile +++ b/test/doc/Makefile @@ -8,15 +8,13 @@ SPHINXBUILD = sphinx-build PAPER = BUILD_DOC_ROOT = $(BR)/test-doc BUILD_DOC_DIR = $(BUILD_DOC_ROOT)/build -API_DOC_GEN_DIR = $(BUILD_DOC_ROOT)/apidoc # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILD_DOC_DIR)/.sphinx-cache $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(API_DOC_GEN_DIR) -c $(SRC_DOC_DIR) +ALLSPHINXOPTS = -d $(BUILD_DOC_DIR)/.sphinx-cache $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SRC_DOC_DIR) # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -INDEX_REL_PATH:=$(shell realpath --relative-to=$(API_DOC_GEN_DIR) $(SRC_DOC_DIR)/index.rst) IN_VENV:=$(shell if pip -V | grep "virtualenv" 2>&1 > /dev/null; then echo 1; else echo 0; fi) .PHONY: verify-virtualenv @@ -25,13 +23,6 @@ ifeq ($(IN_VENV),0) $(error "Not running inside virtualenv (are you running 'make test-doc' from root?)") endif -.PHONY: regen-api-doc -regen-api-doc: verify-virtualenv - @mkdir -p $(API_DOC_GEN_DIR) - #@echo ".. include:: $(INDEX_REL_PATH)" > $(API_DOC_GEN_DIR)/index.rst - @cp $(SRC_DOC_DIR)/index.rst $(API_DOC_GEN_DIR) - sphinx-apidoc -o $(API_DOC_GEN_DIR) .. - .PHONY: help help: @echo "Please use \`make ' where is one of" @@ -67,44 +58,44 @@ wipe: rm -rf $(BUILD_DOC_ROOT) .PHONY: html -html: regen-api-doc verify-virtualenv +html: verify-virtualenv $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/html." .PHONY: dirhtml -dirhtml: regen-api-doc verify-virtualenv +dirhtml: verify-virtualenv $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/dirhtml." .PHONY: singlehtml -singlehtml: regen-api-doc verify-virtualenv +singlehtml: verify-virtualenv $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILD_DOC_DIR)/singlehtml." .PHONY: pickle -pickle: regen-api-doc verify-virtualenv +pickle: verify-virtualenv $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json -json: regen-api-doc verify-virtualenv +json: verify-virtualenv $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp -htmlhelp: regen-api-doc verify-virtualenv +htmlhelp: verify-virtualenv $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILD_DOC_DIR)/htmlhelp." .PHONY: qthelp -qthelp: regen-api-doc verify-virtualenv +qthelp: verify-virtualenv $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -114,7 +105,7 @@ qthelp: regen-api-doc verify-virtualenv @echo "# assistant -collectionFile $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhc" .PHONY: applehelp -applehelp: regen-api-doc verify-virtualenv +applehelp: verify-virtualenv $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILD_DOC_DIR)/applehelp." @@ -123,7 +114,7 @@ applehelp: regen-api-doc verify-virtualenv "bundle." .PHONY: devhelp -devhelp: regen-api-doc verify-virtualenv +devhelp: verify-virtualenv $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/devhelp @echo @echo "Build finished." @@ -133,7 +124,7 @@ devhelp: regen-api-doc verify-virtualenv @echo "# devhelp" .PHONY: epub -epub: regen-api-doc verify-virtualenv +epub: verify-virtualenv $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub @echo @echo "Build finished. The epub file is in $(BUILD_DOC_DIR)/epub." @@ -145,7 +136,7 @@ epub3: @echo "Build finished. The epub3 file is in $(BUILD_DOC_DIR)/epub3." .PHONY: latex -latex: regen-api-doc verify-virtualenv +latex: verify-virtualenv $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILD_DOC_DIR)/latex." @@ -153,33 +144,33 @@ latex: regen-api-doc verify-virtualenv "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf -latexpdf: regen-api-doc verify-virtualenv +latexpdf: verify-virtualenv $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: latexpdfja -latexpdfja: regen-api-doc verify-virtualenv +latexpdfja: verify-virtualenv $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: text -text: regen-api-doc verify-virtualenv +text: verify-virtualenv $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/text @echo @echo "Build finished. The text files are in $(BUILD_DOC_DIR)/text." .PHONY: man -man: regen-api-doc verify-virtualenv +man: verify-virtualenv $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/man @echo @echo "Build finished. The manual pages are in $(BUILD_DOC_DIR)/man." .PHONY: texinfo -texinfo: regen-api-doc verify-virtualenv +texinfo: verify-virtualenv $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILD_DOC_DIR)/texinfo." @@ -187,57 +178,57 @@ texinfo: regen-api-doc verify-virtualenv "(use \`make info' here to do that automatically)." .PHONY: info -info: regen-api-doc verify-virtualenv +info: verify-virtualenv $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILD_DOC_DIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILD_DOC_DIR)/texinfo." .PHONY: gettext -gettext: regen-api-doc verify-virtualenv +gettext: verify-virtualenv $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILD_DOC_DIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILD_DOC_DIR)/locale." .PHONY: changes -changes: regen-api-doc verify-virtualenv +changes: verify-virtualenv $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/changes @echo @echo "The overview file is in $(BUILD_DOC_DIR)/changes." .PHONY: linkcheck -linkcheck: regen-api-doc verify-virtualenv +linkcheck: verify-virtualenv $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILD_DOC_DIR)/linkcheck/output.txt." .PHONY: doctest -doctest: regen-api-doc verify-virtualenv +doctest: verify-virtualenv $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILD_DOC_DIR)/doctest/output.txt." .PHONY: coverage -coverage: regen-api-doc verify-virtualenv +coverage: verify-virtualenv $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILD_DOC_DIR)/coverage/python.txt." .PHONY: xml -xml: regen-api-doc verify-virtualenv +xml: verify-virtualenv $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/xml @echo @echo "Build finished. The XML files are in $(BUILD_DOC_DIR)/xml." .PHONY: pseudoxml -pseudoxml: regen-api-doc verify-virtualenv +pseudoxml: verify-virtualenv $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILD_DOC_DIR)/pseudoxml." .PHONY: dummy -dummy: regen-api-doc verify-virtualenv +dummy: verify-virtualenv $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." diff --git a/test/doc/conf.py b/test/doc/conf.py index cec9ddee..96ea50fe 100644 --- a/test/doc/conf.py +++ b/test/doc/conf.py @@ -88,11 +88,11 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. # -# default_role = None +default_role = 'any' # If true, '()' will be appended to :func: etc. cross-reference text. # -# add_function_parentheses = True +add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). @@ -156,7 +156,7 @@ html_theme = 'alabaster' # 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'] +# html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied diff --git a/test/doc/framework.rst b/test/doc/framework.rst new file mode 100644 index 00000000..ce5e16e2 --- /dev/null +++ b/test/doc/framework.rst @@ -0,0 +1,7 @@ +framework module +================ + +.. automodule:: framework + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/hook.rst b/test/doc/hook.rst new file mode 100644 index 00000000..f856e519 --- /dev/null +++ b/test/doc/hook.rst @@ -0,0 +1,7 @@ +hook module +=========== + +.. automodule:: hook + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/index.rst b/test/doc/index.rst index c18357a4..323b608f 100644 --- a/test/doc/index.rst +++ b/test/doc/index.rst @@ -1,15 +1,158 @@ -.. 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. +.. _unittest: https://docs.python.org/2/library/unittest.html +.. _TestCase: https://docs.python.org/2/library/unittest.html#unittest.TestCase +.. _AssertionError: https://docs.python.org/2/library/exceptions.html#exceptions.AssertionError +.. _SkipTest: https://docs.python.org/2/library/unittest.html#unittest.SkipTest +.. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/ +.. _scapy: http://www.secdev.org/projects/scapy/ -Welcome to VPP test framework's documentation! -============================================== +.. |vtf| replace:: VPP Test Framework -Contents: +|vtf| +===== +Overview +######## + +The goal of the |vtf| is to ease writing, running and debugging +unit tests for the VPP. For this, python was chosen as a high level language +allowing rapid development with scapy_ providing the necessary tool for creating +and dissecting packets. + +Anatomy of a test case +###################### + +Python's unittest_ is used as the base framework upon which the VPP test +framework is built. A test suite in the |vtf| constists of multiple classes +derived from `VppTestCase`, which is itself derived from TestCase_. +The test class defines one or more test functions, which act as test cases. + +Function flow when running a test case is: + +1. `setUpClass `: + This function is called once for each test class, allowing a one-time test + setup to be executed. If this functions throws an exception, + none of the test functions are executed. +2. `setUp `: + The setUp function runs before each of the test functions. If this function + throws an exception other than AssertionError_ or SkipTest_, then this is + considered an error, not a test failure. +3. *test_*: + This is the guts of the test case. It should execute the test scenario + and use the various assert functions from the unittest framework to check + necessary. +4. `tearDown `: + The tearDown function is called after each test function with the purpose + of doing partial cleanup. +5. `tearDownClass `: + Method called once after running all of the test funnctions to perform + the final cleanup. + +Test temporary directory and VPP life cycle +################################################ + +Test separation is achieved by separating the test files and vpp instances. +Each test creates a temporary directory and it's name is used to create +a shared memory prefix which is used to run a VPP instance. +This way, there is no conflict between any other VPP instances running +on the box and the test VPP. Any temporary files created by the test case +are stored in this temporary test directory. + +Virtual environment +################### + +Virtualenv_ is a python module which provides a means to crate an environment +containing the dependencies required by the |vtf|, allowing a separation +from any existing system-wide packages. |vtf|'s Makefile automatically +creates a virtualenv_ inside build-root and installs the required packages +in that environment. The environment is entered whenever executing a test +via one of the make test targets. + +Naming conventions +################## + +Most unit tests do some kind of packet manipulation - sending and receiving +packets between VPP and virtual hosts connected to the VPP. Referring +to the sides, addresses, etc.. is always done as if looking from the VPP side, +thus: + +* *local_* prefix is used for the VPP side. + So e.g. `local_ip4 ` address is the IPv4 address + assigned to the VPP interface. +* *remote_* prefix is used for the virtual host side. + So e.g. `remote_mac ` address is the MAC address + assigned to the virtual host connected to the VPP. + +Packet flow in the |vtf| +######################## + +Test framework -> VPP +~~~~~~~~~~~~~~~~~~~~~ + +|vtf| doesn't send any packets to VPP directly. Traffic is instead injected +using packet-generator interfaces, represented by the `VppPGInterface` class. +Packets are written into a temporary .pcap file, which is then read by the VPP +and the packets are injected into the VPP world. + +VPP -> test framework +~~~~~~~~~~~~~~~~~~~~~ + +Similarly, VPP doesn't send any packets to |vtf| directly. Instead, packet +capture feature is used to capture and write traffic to a temporary .pcap file, +which is then read and analyzed by the |vtf|. + +Test framework objects +###################### + +The following objects provide VPP abstraction and provide a means to do +common tasks easily in the test cases. + +* `VppInterface`: abstract class representing generic VPP interface + and contains some common functionality, which is then used by derived classes +* `VppPGInterface`: class representing VPP packet-generator interface. + The interface is created/destroyed when the object is created/destroyed. +* `VppSubInterface`: VPP sub-interface abstract class, containing common + functionality for e.g. `VppDot1QSubint` and `VppDot1ADSubint` classes + +How VPP API/CLI is called +######################### + +Vpp provides python bindings in a python module called vpp-papi, which the test +framework installs in the virtual environment. A shim layer represented by +the `VppPapiProvider` class is built on top of the vpp-papi, serving these +purposes: + +1. Automatic return value checks: + After each API is called, the return value is checked against the expected + return value (by default 0, but can be overridden) and an exception + is raised if the check fails. +2. Automatic call of hooks: + + a. `before_cli ` and `before_api ` hooks + are used for debug logging and stepping through the test + b. `after_cli ` and `after_api ` hooks + are used for monitoring the vpp process for crashes +3. Simplification of API calls: + Many of the VPP APIs take a lot of parameters and by providing sane defaults + for these, the API is much easier to use in the common case and the code is + more readable. E.g. ip_add_del_route API takes ~25 parameters, of which + in the common case, only 3 are needed. + +Example: how to add a new test +############################## + +In this example, we will describe how to add a new test case which tests VPP... + +1. Add a new file called... +2. Add a setUpClass function containing... +3. Add the test code in test... +4. Run the test... + +|vtf| module documentation +########################## + .. toctree:: :maxdepth: 2 + :glob: modules.rst diff --git a/test/doc/log.rst b/test/doc/log.rst new file mode 100644 index 00000000..5965d5ad --- /dev/null +++ b/test/doc/log.rst @@ -0,0 +1,7 @@ +log module +========== + +.. automodule:: log + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/modules.rst b/test/doc/modules.rst new file mode 100644 index 00000000..6d456d7f --- /dev/null +++ b/test/doc/modules.rst @@ -0,0 +1,24 @@ +test +==== + +.. toctree:: + :maxdepth: 4 + + framework + hook + log + run_tests + scapy_handlers + template_bd + test_ip + test_ip6 + test_l2bd + test_l2xc + test_lb + test_mpls + test_vxlan + util + vpp_interface + vpp_papi_provider + vpp_pg_interface + vpp_sub_interface diff --git a/test/doc/run_tests.rst b/test/doc/run_tests.rst new file mode 100644 index 00000000..30cec4e3 --- /dev/null +++ b/test/doc/run_tests.rst @@ -0,0 +1,7 @@ +run_tests module +================ + +.. automodule:: run_tests + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/scapy_handlers.rst b/test/doc/scapy_handlers.rst new file mode 100644 index 00000000..6717326a --- /dev/null +++ b/test/doc/scapy_handlers.rst @@ -0,0 +1,22 @@ +scapy_handlers package +====================== + +Submodules +---------- + +scapy_handlers.vxlan module +--------------------------- + +.. automodule:: scapy_handlers.vxlan + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: scapy_handlers + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/template_bd.rst b/test/doc/template_bd.rst new file mode 100644 index 00000000..e173d27a --- /dev/null +++ b/test/doc/template_bd.rst @@ -0,0 +1,7 @@ +template_bd module +================== + +.. automodule:: template_bd + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_ip.rst b/test/doc/test_ip.rst new file mode 100644 index 00000000..a1e97b86 --- /dev/null +++ b/test/doc/test_ip.rst @@ -0,0 +1,7 @@ +test_ip module +============== + +.. automodule:: test_ip + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_ip6.rst b/test/doc/test_ip6.rst new file mode 100644 index 00000000..63461c3c --- /dev/null +++ b/test/doc/test_ip6.rst @@ -0,0 +1,7 @@ +test_ip6 module +=============== + +.. automodule:: test_ip6 + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_l2bd.rst b/test/doc/test_l2bd.rst new file mode 100644 index 00000000..8a80dfe6 --- /dev/null +++ b/test/doc/test_l2bd.rst @@ -0,0 +1,7 @@ +test_l2bd module +================ + +.. automodule:: test_l2bd + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_l2xc.rst b/test/doc/test_l2xc.rst new file mode 100644 index 00000000..caf99bad --- /dev/null +++ b/test/doc/test_l2xc.rst @@ -0,0 +1,7 @@ +test_l2xc module +================ + +.. automodule:: test_l2xc + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_lb.rst b/test/doc/test_lb.rst new file mode 100644 index 00000000..e134b29c --- /dev/null +++ b/test/doc/test_lb.rst @@ -0,0 +1,7 @@ +test_lb module +============== + +.. automodule:: test_lb + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_mpls.rst b/test/doc/test_mpls.rst new file mode 100644 index 00000000..e4c8ce0e --- /dev/null +++ b/test/doc/test_mpls.rst @@ -0,0 +1,7 @@ +test_mpls module +================ + +.. automodule:: test_mpls + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/test_vxlan.rst b/test/doc/test_vxlan.rst new file mode 100644 index 00000000..7f40c319 --- /dev/null +++ b/test/doc/test_vxlan.rst @@ -0,0 +1,7 @@ +test_vxlan module +================= + +.. automodule:: test_vxlan + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/util.rst b/test/doc/util.rst new file mode 100644 index 00000000..d8ad0d14 --- /dev/null +++ b/test/doc/util.rst @@ -0,0 +1,7 @@ +util module +=========== + +.. automodule:: util + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/vpp_interface.rst b/test/doc/vpp_interface.rst new file mode 100644 index 00000000..32bb009b --- /dev/null +++ b/test/doc/vpp_interface.rst @@ -0,0 +1,7 @@ +vpp_interface module +==================== + +.. automodule:: vpp_interface + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/vpp_papi_provider.rst b/test/doc/vpp_papi_provider.rst new file mode 100644 index 00000000..c9efe85a --- /dev/null +++ b/test/doc/vpp_papi_provider.rst @@ -0,0 +1,7 @@ +vpp_papi_provider module +======================== + +.. automodule:: vpp_papi_provider + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/vpp_pg_interface.rst b/test/doc/vpp_pg_interface.rst new file mode 100644 index 00000000..85f54f62 --- /dev/null +++ b/test/doc/vpp_pg_interface.rst @@ -0,0 +1,7 @@ +vpp_pg_interface module +======================= + +.. automodule:: vpp_pg_interface + :members: + :undoc-members: + :show-inheritance: diff --git a/test/doc/vpp_sub_interface.rst b/test/doc/vpp_sub_interface.rst new file mode 100644 index 00000000..5ff5300b --- /dev/null +++ b/test/doc/vpp_sub_interface.rst @@ -0,0 +1,7 @@ +vpp_sub_interface module +======================== + +.. automodule:: vpp_sub_interface + :members: + :undoc-members: + :show-inheritance: diff --git a/test/test_infra.md b/test/test_infra.md deleted file mode 100644 index 371d48b0..00000000 --- a/test/test_infra.md +++ /dev/null @@ -1,145 +0,0 @@ -# VPP Functional Test Infra - -## Running VPP tests -VPP functional tests are triggered by `make test` command run in the git vpp source directory. Following Linux environment variables are used by the current VPP functional test infrastructure: - -- `TEST=` - run only specific test identified by filename `test/test_.py` -- `V=[0|1|2]` - set verbosity level. `0` for minimal verbosity, `1` for increased verbosity, `2` for maximum verbosity. Default value is 0. - -Example of running tests: - -``` -~/src/vpp-test-infra$ make test V=1 TEST=vxlan -``` - -All tests listed in `test/` directory are run by default. To run selected tests you can set variable TEST when starting tests. - -## Overview -The main functionality of the test framework is defined in [framework.py](test/framework.py) file. The implementation of the test framework uses classes and methods from Python module *unittest*. - -Three main classes are defined to support the overall test automation: - -* **class VppTestCase(unittest.TestCase)** - a sub-class of *unittest.TestCase* class. Provides methods to create and run test case. These methods can be divided into 5 groups: - 1. Methods to control test case setup and tear down: - * *def setUpConstants(cls):* - * *def setUpClass(cls):* - * *def quit(cls):* - * *def tearDownClass(cls):* - * *def tearDown(self):* - * *def setUp(self):* - - 2. Methods to create VPP packet generator interfaces: - * *def create_interfaces(cls, args):* - - 3. Methods to execute VPP commands and print logs in the output (terminal for now): - * *def log(cls, s, v=1):* - * *def api(cls, s):* - * *def cli(cls, v, s):* - - 4. Methods to control packet stream generation and capturing: - * *def pg_add_stream(cls, i, pkts):* - * *def pg_enable_capture(cls, args):* - * *def pg_start(cls):* - * *def pg_get_capture(cls, o):* - - 5. Methods to create and verify packets: - * *def extend_packet(packet, size):* - * *def add_packet_info_to_list(self, info):* - * *def create_packet_info(self, pg_id, target_id):* - * *def info_to_payload(info):* - * *def payload_to_info(payload):* - * *def get_next_packet_info(self, info):* - * *def get_next_packet_info_for_interface(self, src_pg, info):* - * *def get_next_packet_info_for_interface2(self, src_pg, dst_pg, info):* - -* **class VppTestResult(unittest.TestResult)** - a sub-class of *unittest.TestResult* class. Provides methods to compile information about the tests that have succeeded and the ones that have failed. These methods can be divided into 4 groups: - 1. Processing test case result: - * *def addSuccess(self, test):* - * *def addFailure(self, test, err):* - * *def addError(self, test, err):* - - 2. Processing test case description: - * *def getDescription(self, test):* - - 3. Processing test case start and stop: - * *def startTest(self, test):* - * *def stopTest(self, test):* - - 4. Printing error and failure information: - * *def printErrors(self):* - * *def printErrorList(self, flavour, errors):* - -* **class VppTestRunner(unittest.TextTestRunner)** - a sub-class of *unittest.TextTestRunner* class. Provides basic test runner implementation that prints results on standard error stream. Contains one method: - * *def run(self, test):* - -In addition [util.py] (test/util.py) file defines number of common methods useful for many test cases. All of these methods are currently contained in one class: - -* **class Util(object)**: - * *def resolve_arp(cls, args):* - * *def resolve_icmpv6_nd(cls, args):* - * *def config_ip4(cls, args):* - * *def config_ip6(cls, args):* - -## Interaction with VPP -VPP is started from command line as a sub-process during the test case setup phase. Command line attributes to start VPP are stored in class variable *vpp_cmdline*. -To get an overview of VPP command line attributes, visit section [Command-line Arguments](https://wiki.fd.io/view/VPP/Command-line_Arguments) on VPP wiki page. - -Current VPP test infrastructure is using two ways to interact with VPP for configuration, operational status check, tracing and logging. - -### Using API commands -API commands are executed by VPP API test tool that is started from command line as a sub-process. Command line attributes to start VPP API test tool are stored in class variable *vpp_api_test_cmdline*. -When executed, API command and its possible output are printed in the terminal if verbosity level is greater then 0. - -Example: - -``` -cls.api("sw_interface_set_flags pg1 admin-up") -``` - -will print in the terminal - -``` -API: sw_interface_set_flags pg1 admin-up -``` - -### Using CLI commands -CLI commands are executed via VPP API test tool by sending API command "*exec + cli_command*". It is possible to set verbosity level for executing specific CLI commands, so that the CLI command is executed only and only if its associated verbosity level is equal or lower then the verbosity level set in the system. - -Similarly to API commands, when executed, CLI command and its possible output are printed in the terminal if verbosity level is greater then 0. - -Example I - CLI command will be executed always (its verbosity is 0): - -``` -cls.cli(0, "show l2fib") -``` - -Example II - CLI command will be executed only if the verbosity level is set to 2: - -``` -self.cli(2, "show l2fib verbose") -``` - -## Logging -It is possible to log some additional information in the terminal for different verbosity levels. - -Example I - verbosity level of the log is set to default value (0): - -``` -self.log("Verifying capture %u" % i) -``` - -will be always printed in the terminal: - -``` -LOG: Verifying capture 0 -``` - -Example II - the log will be printed in the terminal only if the verbosity level is set to 2: - -``` -self.log("Got packet on port %u: src=%u (id=%u)" - % (o, payload_info.src, payload_info.index), 2) -``` - ---- -***END*** diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index fc4080d2..c9e4076d 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,4 +1,5 @@ import os +import time from logging import error from scapy.utils import wrpcap, rdpcap from vpp_interface import VppInterface @@ -39,11 +40,27 @@ class VppPGInterface(VppInterface): """CLI string to load the injected packets""" return self._input_cli + @property + def in_history_counter(self): + """Self-incrementing counter used when renaming old pcap files""" + v = self._in_history_counter + self._in_history_counter += 1 + return v + + @property + def out_history_counter(self): + """Self-incrementing counter used when renaming old pcap files""" + v = self._out_history_counter + self._out_history_counter += 1 + return v + 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._out_file = "pg%u_out.pcap" % self.sw_if_index + self._out_path = self.test.tempdir + "/" + self._out_file + self._in_file = "pg%u_in.pcap" % self.sw_if_index + self._in_path = self.test.tempdir + "/" + self._in_file 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 @@ -52,6 +69,8 @@ class VppPGInterface(VppInterface): def __init__(self, test, pg_index): """ Create VPP packet-generator interface """ + self._in_history_counter = 0 + self._out_history_counter = 0 self._pg_index = pg_index self._test = test r = self.test.vapi.pg_create_interface(self.pg_index) @@ -61,7 +80,14 @@ class VppPGInterface(VppInterface): def enable_capture(self): """ Enable capture on this packet-generator interface""" try: - os.unlink(self.out_path) + 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)) except: pass # FIXME this should be an API, but no such exists atm @@ -75,7 +101,14 @@ class VppPGInterface(VppInterface): """ try: - os.remove(self.in_path) + 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)) except: pass wrpcap(self.in_path, pkts) -- cgit 1.2.3-korg From 177bbdcd8fa4e7621c5bdd3afd8c6e74b603e096 Mon Sep 17 00:00:00 2001 From: Neale Ranns Date: Tue, 15 Nov 2016 09:46:51 +0000 Subject: GRE tests and fixes Change-Id: I234240e9bdd4b69ad64a17b1449ae1e81c0edaca Signed-off-by: Neale Ranns --- .gitignore | 3 + test/Makefile | 30 +- test/framework.py | 2 +- test/patches/scapy-2.3.3/gre-layers.patch | 25 ++ test/test_gre.py | 700 ++++++++++++++++++++++++++++++ test/vpp_gre_interface.py | 34 ++ test/vpp_interface.py | 31 ++ test/vpp_ip_route.py | 46 ++ test/vpp_papi_provider.py | 51 +++ test/vpp_sub_interface.py | 3 + vnet/vnet/adj/adj.c | 10 +- vnet/vnet/adj/adj_internal.h | 3 +- vnet/vnet/adj/adj_nbr.c | 4 +- vnet/vnet/ethernet/arp.c | 2 +- vnet/vnet/gre/gre.c | 43 +- vnet/vnet/gre/gre.h | 77 +++- vnet/vnet/gre/interface.c | 51 ++- vnet/vnet/gre/node.c | 21 +- vnet/vnet/interface_output.c | 29 +- vnet/vnet/ip/ip6_forward.c | 2 + vnet/vnet/ip/ip6_neighbor.c | 2 - vpp/vpp-api/api.c | 2 +- 22 files changed, 1095 insertions(+), 76 deletions(-) create mode 100644 test/patches/scapy-2.3.3/gre-layers.patch create mode 100644 test/test_gre.py create mode 100644 test/vpp_gre_interface.py create mode 100644 test/vpp_ip_route.py (limited to 'test/Makefile') diff --git a/.gitignore b/.gitignore index 5a8384a8..cec432c5 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ GTAGS /vpp-api/python/vpp_papi.egg-info /vpp-api/python/vpp_papi/memclnt.py /vpp-api/python/vpp_papi/vpe.py + +# Build files in the test directory +/test/*.ok diff --git a/test/Makefile b/test/Makefile index aac637d3..de6aaa7a 100644 --- a/test/Makefile +++ b/test/Makefile @@ -6,23 +6,45 @@ ifndef VPP_PYTHON_PREFIX endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=scapy pexpect +PYTHON_DEPENDS=scapy==2.3.3 pexpect +SCAPY_SOURCE=$(WS_ROOT)/build-root/python/virtualenv/lib/python2.7/site-packages/ -test: wipe verify-python-path + +.pip-install.ok: @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS)" + @touch $@ + +.pip-patch.ok: .pip-install.ok + @echo --- patching --- + for f in $(CURDIR)/patches/scapy-2.3.3/*.patch ; do \ + echo Applying patch: $$(basename $$f) ; \ + patch -p1 -d $(SCAPY_SOURCE) < $$f ; \ + done + @touch $@ + +.install.ok: .pip-patch.ok @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && cd $(WS_ROOT)/vpp-api/python && python setup.py install" + @touch $@ + +PHONIES=.install.ok .pip-patch.ok .pip-install.ok +.PHONY: $(PHONIES) + +test: reset verify-python-path .install.ok @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" -retest: wipe verify-python-path +retest: reset verify-python-path @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" .PHONY: wipe doc -wipe: +reset: @rm -f /dev/shm/vpp-unittest-* @rm -rf /tmp/vpp-unittest-* +wipe: reset + @rm -f $(PHONIES) + doc: verify-python-path @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx" diff --git a/test/framework.py b/test/framework.py index 227428ef..1375f076 100644 --- a/test/framework.py +++ b/test/framework.py @@ -265,8 +265,8 @@ class VppTestCase(unittest.TestCase): def tearDown(self): """ Show various debug prints after each test """ if not self.vpp_dead: - self.logger.info(self.vapi.ppcli("show int")) self.logger.debug(self.vapi.cli("show trace")) + self.logger.info(self.vapi.ppcli("show int")) self.logger.info(self.vapi.ppcli("show hardware")) self.logger.info(self.vapi.ppcli("show error")) self.logger.info(self.vapi.ppcli("show run")) diff --git a/test/patches/scapy-2.3.3/gre-layers.patch b/test/patches/scapy-2.3.3/gre-layers.patch new file mode 100644 index 00000000..605a705b --- /dev/null +++ b/test/patches/scapy-2.3.3/gre-layers.patch @@ -0,0 +1,25 @@ +diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py +index 03b80ec..a7e1e0f 100644 +--- a/scapy/layers/inet6.py ++++ b/scapy/layers/inet6.py +@@ -3722,6 +3722,7 @@ conf.l2types.register(31, IPv6) + + bind_layers(Ether, IPv6, type = 0x86dd ) + bind_layers(CookedLinux, IPv6, proto = 0x86dd ) ++bind_layers(GRE, IPv6, proto = 0x86dd ) + bind_layers(IPerror6, TCPerror, nh = socket.IPPROTO_TCP ) + bind_layers(IPerror6, UDPerror, nh = socket.IPPROTO_UDP ) + bind_layers(IPv6, TCP, nh = socket.IPPROTO_TCP ) +diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py +index 4f491d2..661a5da 100644 +--- a/scapy/layers/l2.py ++++ b/scapy/layers/l2.py +@@ -628,7 +628,7 @@ bind_layers( CookedLinux, EAPOL, proto=34958) + bind_layers( GRE, LLC, proto=122) + bind_layers( GRE, Dot1Q, proto=33024) + bind_layers( GRE, Dot1AD, type=0x88a8) +-bind_layers( GRE, Ether, proto=1) ++bind_layers( GRE, Ether, proto=0x6558) + bind_layers( GRE, ARP, proto=2054) + bind_layers( GRE, EAPOL, proto=34958) + bind_layers( GRE, GRErouting, { "routing_present" : 1 } ) diff --git a/test/test_gre.py b/test/test_gre.py new file mode 100644 index 00000000..b5a1e346 --- /dev/null +++ b/test/test_gre.py @@ -0,0 +1,700 @@ +#!/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_gre_interface import VppGreInterface +from vpp_ip_route import IpRoute, IpPath +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.inet import IP, UDP +from scapy.layers.inet6 import ICMPv6ND_NS, IPv6, UDP +from scapy.contrib.mpls import MPLS +from scapy.volatile import RandMAC, RandIP + + +class TestGRE(VppTestCase): + """ GRE Test Case """ + + @classmethod + def setUpClass(cls): + super(TestGRE, cls).setUpClass() + + def setUp(self): + super(TestGRE, self).setUp() + + # create 2 pg interfaces - set one in a non-default table. + self.create_pg_interfaces(range(2)) + + self.pg1.set_table_ip4(1) + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + def tearDown(self): + super(TestGRE, self).tearDown() + + 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) + 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) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_4o4(self, src_if, + tunnel_src, tunnel_dst, + 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) + 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) / + GRE() / + IP(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_6o4(self, src_if, + tunnel_src, tunnel_dst, + 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) + 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) / + GRE() / + IPv6(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_l2o4(self, src_if, + 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) + 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) / + GRE() / + Ether(dst=RandMAC('*:*:*:*:*:*'), + src=RandMAC('*:*:*:*:*:*')) / + IP(src=str(RandIP()), dst=str(RandIP())) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_vlano4(self, src_if, + 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) + 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) / + GRE() / + Ether(dst=RandMAC('*:*:*:*:*:*'), + src=RandMAC('*:*:*:*:*:*')) / + Dot1Q(vlan=vlan) / + IP(src=str(RandIP()), dst=str(RandIP())) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def verify_filter(self, capture, sent): + if not len(capture) == len(sent): + # filter out any IPv6 RAs from the captur + for p in capture: + if (p.haslayer(ICMPv6ND_RA)): + capture.remove(p) + return capture + + def verify_tunneled_4o4(self, src_if, capture, sent, + tunnel_src, tunnel_dst): + + capture = self.verify_filter(capture, sent) + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_ip = rx_gre[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + rx.show() + tx.show() + raise + + def verify_tunneled_l2o4(self, src_if, capture, sent, + tunnel_src, tunnel_dst): + capture = self.verify_filter(capture, sent) + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_l2 = rx_gre[Ether] + rx_ip = rx_l2[IP] + tx_gre = tx[GRE] + tx_l2 = tx_gre[Ether] + tx_ip = tx_l2[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # bridged, not L3 forwarded, so no TTL decrement + self.assertEqual(rx_ip.ttl, tx_ip.ttl) + + except: + rx.show() + tx.show() + raise + + def verify_tunneled_vlano4(self, src_if, capture, sent, + tunnel_src, tunnel_dst, vlan): + try: + capture = self.verify_filter(capture, sent) + self.assertEqual(len(capture), len(sent)) + except: + capture.show() + raise + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_l2 = rx_gre[Ether] + rx_vlan = rx_l2[Dot1Q] + rx_ip = rx_l2[IP] + + self.assertEqual(rx_vlan.vlan, vlan) + + tx_gre = tx[GRE] + tx_l2 = tx_gre[Ether] + tx_ip = tx_l2[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # bridged, not L3 forwarded, so no TTL decrement + self.assertEqual(rx_ip.ttl, tx_ip.ttl) + + except: + rx.show() + tx.show() + raise + + def verify_decapped_4o4(self, src_if, capture, sent): + capture = self.verify_filter(capture, sent) + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + tx_gre = tx[GRE] + tx_ip = tx_gre[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + rx.show() + tx.show() + raise + + def verify_decapped_6o4(self, src_if, capture, sent): + capture = self.verify_filter(capture, sent) + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IPv6] + tx_gre = tx[GRE] + tx_ip = tx_gre[IPv6] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + self.assertEqual(rx_ip.hlim + 1, tx_ip.hlim) + + except: + rx.show() + tx.show() + raise + + def test_gre(self): + """ GRE tunnel Tests """ + + # + # Create an L3 GRE tunnel. + # - set it admin up + # - assign an IP Addres + # - Add a route via the tunnel + # + gre_if = VppGreInterface(self, + self.pg0.local_ip4, + "1.1.1.2") + gre_if.add_vpp_config() + + # + # The double create (create the same tunnel twice) should fail, + # and we should still be able to use the original + # + try: + gre_if.add_vpp_config() + except Exception: + pass + else: + self.fail("Double GRE tunnel add does not fail") + + gre_if.admin_up() + gre_if.config_ip4() + + route_via_tun = IpRoute(self, "4.4.4.4", 32, + [IpPath("0.0.0.0", gre_if.sw_if_index)]) + + route_via_tun.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - they are all dropped since the tunnel's desintation IP + # is unresolved - or resolves via the default route - which + # which is a drop. + # + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "4.4.4.4") + self.pg0.add_stream(tx) + + 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 + + # + # Add a route that resolves the tunnel's destination + # + route_tun_dst = IpRoute(self, "1.1.1.2", 32, + [IpPath(self.pg0.remote_ip4, self.pg0.sw_if_index)]) + route_tun_dst.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - packets are GRE encapped + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "4.4.4.4") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_tunneled_4o4(self.pg0, rx, tx, + self.pg0.local_ip4, "1.1.1.2") + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_decapped_4o4(self.pg0, rx, tx) + + # + # Send tunneled packets that do not match the tunnel's src + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg0, + "1.1.1.3", + self.pg0.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg0.add_stream(tx) + + 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 + + # + # Configure IPv6 on the PG interface so we can route IPv6 + # packets + # + self.pg0.config_ip6() + self.pg0.resolve_ndp() + + # + # Send IPv6 tunnel encapslated packets + # - dropped since IPv6 is not enabled on the tunnel + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_6o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip6, + self.pg0.remote_ip6) + self.pg0.add_stream(tx) + + 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 + + # + # Enable IPv6 on the tunnel + # + gre_if.config_ip6() + + # + # Send IPv6 tunnel encapslated packets + # - forwarded since IPv6 is enabled on the tunnel + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_6o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip6, + self.pg0.remote_ip6) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_decapped_6o4(self.pg0, rx, tx) + + # + # test case cleanup + # + route_tun_dst.remove_vpp_config() + route_via_tun.remove_vpp_config() + gre_if.remove_vpp_config() + + self.pg0.unconfig_ip6() + + def test_gre_vrf(self): + """ GRE tunnel VRF Tests """ + + # + # Create an L3 GRE tunnel whose destination is in the non-default + # table. The underlay is thus non-default - the overlay is still + # the default. + # - set it admin up + # - assign an IP Addres + # + gre_if = VppGreInterface(self, self.pg1.local_ip4, + "2.2.2.2", + outer_fib_id=1) + gre_if.add_vpp_config() + gre_if.admin_up() + gre_if.config_ip4() + + # + # Add a route via the tunnel - in the overlay + # + route_via_tun = IpRoute(self, "9.9.9.9", 32, + [IpPath("0.0.0.0", gre_if.sw_if_index)]) + route_via_tun.add_vpp_config() + + # + # Add a route that resolves the tunnel's destination - in the + # underlay table + # + route_tun_dst = IpRoute(self, "2.2.2.2", 32, table_id=1, + paths=[IpPath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_tun_dst.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # packets are sent in on pg0 which is in the default table + # - packets are GRE encapped + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "9.9.9.9") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture() + self.verify_tunneled_4o4(self.pg1, rx, tx, + self.pg1.local_ip4, "2.2.2.2") + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded. This tests the decap lookup + # does not happen in the encap table + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg1, + "2.2.2.2", + self.pg1.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg1.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_decapped_4o4(self.pg0, rx, tx) + + # + # test case cleanup + # + route_tun_dst.remove_vpp_config() + route_via_tun.remove_vpp_config() + gre_if.remove_vpp_config() + + def test_gre_l2(self): + """ GRE tunnel L2 Tests """ + + # + # Add routes to resolve the tunnel destinations + # + route_tun1_dst = IpRoute(self, "2.2.2.2", 32, + [IpPath(self.pg0.remote_ip4, + self.pg0.sw_if_index)]) + route_tun2_dst = IpRoute(self, "2.2.2.3", 32, + [IpPath(self.pg0.remote_ip4, + self.pg0.sw_if_index)]) + + route_tun1_dst.add_vpp_config() + route_tun2_dst.add_vpp_config() + + # + # Create 2 L2 GRE tunnels and x-connect them + # + gre_if1 = VppGreInterface(self, self.pg0.local_ip4, + "2.2.2.2", + is_teb=1) + gre_if2 = VppGreInterface(self, self.pg0.local_ip4, + "2.2.2.3", + is_teb=1) + gre_if1.add_vpp_config() + gre_if2.add_vpp_config() + + gre_if1.admin_up() + gre_if2.admin_up() + + self.vapi.sw_interface_set_l2_xconnect(gre_if1.sw_if_index, + gre_if2.sw_if_index, + enable=1) + self.vapi.sw_interface_set_l2_xconnect(gre_if2.sw_if_index, + gre_if1.sw_if_index, + enable=1) + + # + # Send in tunnel encapped L2. expect out tunnel encapped L2 + # in both directions + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_l2o4(self.pg0, + "2.2.2.2", + self.pg0.local_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_tunneled_l2o4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.3") + + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_l2o4(self.pg0, + "2.2.2.3", + self.pg0.local_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_tunneled_l2o4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.2") + + self.vapi.sw_interface_set_l2_xconnect(gre_if1.sw_if_index, + gre_if2.sw_if_index, + enable=0) + self.vapi.sw_interface_set_l2_xconnect(gre_if2.sw_if_index, + gre_if1.sw_if_index, + enable=0) + + # + # Create a VLAN sub-interfaces on the GRE TEB interfaces + # then x-connect them + # + gre_if_11 = VppDot1QSubint(self, gre_if1, 11) + gre_if_12 = VppDot1QSubint(self, gre_if2, 12) + + # gre_if_11.add_vpp_config() + # gre_if_12.add_vpp_config() + + gre_if_11.admin_up() + gre_if_12.admin_up() + + self.vapi.sw_interface_set_l2_xconnect(gre_if_11.sw_if_index, + gre_if_12.sw_if_index, + enable=1) + self.vapi.sw_interface_set_l2_xconnect(gre_if_12.sw_if_index, + gre_if_11.sw_if_index, + enable=1) + + # + # Configure both to pop thier respective VLAN tags, + # so that during the x-coonect they will subsequently push + # + self.vapi.sw_interface_set_l2_tag_rewrite(gre_if_12.sw_if_index, + L2_VTR_OP.L2_POP_1, + 12) + self.vapi.sw_interface_set_l2_tag_rewrite(gre_if_11.sw_if_index, + L2_VTR_OP.L2_POP_1, + 11) + + # + # Send traffic in both directiond - expect the VLAN tags to + # be swapped. + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_vlano4(self.pg0, + "2.2.2.2", + self.pg0.local_ip4, + 11) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_tunneled_vlano4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.3", + 12) + + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_vlano4(self.pg0, + "2.2.2.3", + self.pg0.local_ip4, + 12) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_tunneled_vlano4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.2", + 11) + + # + # Cleanup Test resources + # + gre_if_11.remove_vpp_config() + gre_if_12.remove_vpp_config() + gre_if1.remove_vpp_config() + gre_if2.remove_vpp_config() + route_tun1_dst.add_vpp_config() + route_tun2_dst.add_vpp_config() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/vpp_gre_interface.py b/test/vpp_gre_interface.py new file mode 100644 index 00000000..b6a66332 --- /dev/null +++ b/test/vpp_gre_interface.py @@ -0,0 +1,34 @@ + +from vpp_interface import VppInterface +import socket + + +class VppGreInterface(VppInterface): + """ + VPP GRE interface + """ + + def __init__(self, test, src_ip, dst_ip, outer_fib_id=0, is_teb=0): + """ Create VPP loopback interface """ + self._test = test + self.t_src = src_ip + self.t_dst = dst_ip + self.t_outer_fib = outer_fib_id + self.t_is_teb = is_teb + + def add_vpp_config(self): + s = socket.inet_pton(socket.AF_INET, self.t_src) + d = socket.inet_pton(socket.AF_INET, self.t_dst) + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_teb=self.t_is_teb) + self._sw_if_index = r.sw_if_index + self.post_init_setup() + + def remove_vpp_config(self): + s = socket.inet_pton(socket.AF_INET, self.t_src) + d = socket.inet_pton(socket.AF_INET, self.t_dst) + self.unconfig() + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_add=0) diff --git a/test/vpp_interface.py b/test/vpp_interface.py index 30ef8ae7..511cf4bc 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -176,6 +176,19 @@ class VppInterface(object): addr_len = 24 self.test.vapi.sw_interface_add_del_address( self.sw_if_index, addr, addr_len) + self.has_ip4_config = True + + def unconfig_ip4(self): + """Remove IPv4 address on the VPP interface""" + try: + if (self.has_ip4_config): + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, + self.local_ip4n, + 24, is_add=0) + except AttributeError: + self.has_ip4_config = False + self.has_ip4_config = False def configure_ipv4_neighbors(self): """For every remote host assign neighbor's MAC to IPv4 addresses.""" @@ -190,6 +203,24 @@ class VppInterface(object): addr_len = 64 self.test.vapi.sw_interface_add_del_address( self.sw_if_index, addr, addr_len, is_ipv6=1) + self.has_ip6_config = True + + def unconfig_ip6(self): + """Remove IPv6 address on the VPP interface""" + try: + if (self.has_ip6_config): + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, + self.local_ip6n, + 64, is_ipv6=1, is_add=0) + except AttributeError: + self.has_ip6_config = False + self.has_ip6_config = False + + def unconfig(self): + """Unconfigure IPv6 and IPv4 address on the VPP interface""" + self.unconfig_ip4() + self.unconfig_ip6() def set_table_ip4(self, table_id): """Set the interface in a IPv4 Table. diff --git a/test/vpp_ip_route.py b/test/vpp_ip_route.py new file mode 100644 index 00000000..78e6aaa2 --- /dev/null +++ b/test/vpp_ip_route.py @@ -0,0 +1,46 @@ +""" + IP Routes + + object abstractions for representing IP routes in VPP +""" + +import socket + + +class IpPath: + + def __init__(self, nh_addr, nh_sw_if_index, nh_table_id=0): + self.nh_addr = socket.inet_pton(socket.AF_INET, nh_addr) + self.nh_itf = nh_sw_if_index + self.nh_table_id = nh_table_id + + +class IpRoute: + """ + IP Route + """ + + def __init__(self, test, dest_addr, + dest_addr_len, paths, table_id=0): + self._test = test + self.paths = paths + self.dest_addr = socket.inet_pton(socket.AF_INET, dest_addr) + self.dest_addr_len = dest_addr_len + self.table_id = table_id + + def add_vpp_config(self): + for path in self.paths: + self._test.vapi.ip_add_del_route(self.dest_addr, + self.dest_addr_len, + path.nh_addr, + path.nh_itf, + table_id=self.table_id) + + def remove_vpp_config(self): + for path in self.paths: + self._test.vapi.ip_add_del_route(self.dest_addr, + self.dest_addr_len, + path.nh_addr, + path.nh_itf, + table_id=self.table_id, + is_add=0) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 2148e94b..23a108d9 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -18,6 +18,9 @@ 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 @@ -230,6 +233,20 @@ class VppPapiProvider(object): return self.api(vpp_papi.sw_interface_set_l2_xconnect, (rx_sw_if_index, tx_sw_if_index, enable)) + 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 + :param sw_if_index - interface the operation is applied to + :param vtr_op - Choose from l2_vtr_op_t enum values + :param push_dot1q - first pushed flag dot1q id set, else dot1ad + :param tag1 - Needed for any push or translate vtr op + :param tag2 - Needed for any push 2 or translate x-2 vtr ops + + """ + return self.api(vpp_papi.l2_interface_vlan_tag_rewrite, + (sw_if_index, vtr_oper, push, tag1, tag2)) + def sw_interface_set_flags(self, sw_if_index, admin_up_down, link_up_down=0, deleted=0): """ @@ -278,6 +295,13 @@ class VppPapiProvider(object): outer_vlan, inner_vlan)) + def delete_subif(self, sw_if_index): + """Delete subinterface + + :param sw_if_index: + """ + return self.api(vpp_papi.delete_subif, ([sw_if_index])) + def create_vlan_subif(self, sw_if_index, vlan): """ @@ -411,3 +435,30 @@ class VppPapiProvider(object): """ return self.api(vpp_papi.sw_interface_span_enable_disable, (sw_if_index_from, sw_if_index_to, enable )) + + def gre_tunnel_add_del(self, + src_address, + dst_address, + outer_fib_id=0, + is_teb=0, + is_add=1, + is_ip6=0): + """ Add a GRE tunnel + + :param src_address: + :param dst_address: + :param outer_fib_id: (Default value = 0) + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param is_teb: (Default value = 0) + """ + + return self.api( + vpp_papi.gre_add_del_tunnel, + (is_add, + is_ip6, + is_teb, + src_address, + dst_address, + outer_fib_id) + ) diff --git a/test/vpp_sub_interface.py b/test/vpp_sub_interface.py index b387d27b..027a24b2 100644 --- a/test/vpp_sub_interface.py +++ b/test/vpp_sub_interface.py @@ -41,6 +41,9 @@ class VppSubInterface(VppPGInterface): def add_dot1_layer(self, pkt): pass + def remove_vpp_config(self): + self.test.vapi.delete_subif(self._sw_if_index) + class VppDot1QSubint(VppSubInterface): diff --git a/vnet/vnet/adj/adj.c b/vnet/vnet/adj/adj.c index 24f7662d..2741c885 100644 --- a/vnet/vnet/adj/adj.c +++ b/vnet/vnet/adj/adj.c @@ -61,6 +61,7 @@ adj_alloc (fib_protocol_t proto) adj->mcast_group_index = ~0; adj->saved_lookup_next_index = 0; adj->n_adj = 1; + adj->lookup_next_index = 0; fib_node_init(&adj->ia_node, FIB_NODE_TYPE_ADJ); @@ -163,7 +164,8 @@ adj_last_lock_gone (ip_adjacency_t *adj) /* * complete and incomplete nbr adjs */ - adj_nbr_remove(adj->ia_nh_proto, + adj_nbr_remove(adj_get_index(adj), + adj->ia_nh_proto, adj->ia_link, &adj->sub_type.nbr.next_hop, adj->rewrite_header.sw_if_index); @@ -376,6 +378,12 @@ adj_show (vlib_main_t * vm, if (ADJ_INDEX_INVALID != ai) { + if (pool_is_free_index(adj_pool, ai)) + { + vlib_cli_output (vm, "adjacency %d invalid", ai); + return 0; + } + vlib_cli_output (vm, "[@%d] %U", ai, format_ip_adjacency, ai, diff --git a/vnet/vnet/adj/adj_internal.h b/vnet/vnet/adj/adj_internal.h index e3e0e04c..833bc7c9 100644 --- a/vnet/vnet/adj/adj_internal.h +++ b/vnet/vnet/adj/adj_internal.h @@ -93,7 +93,8 @@ extern void adj_nbr_update_rewrite_internal (ip_adjacency_t *adj, extern ip_adjacency_t * adj_alloc(fib_protocol_t proto); -extern void adj_nbr_remove(fib_protocol_t nh_proto, +extern void adj_nbr_remove(adj_index_t ai, + fib_protocol_t nh_proto, vnet_link_t link_type, const ip46_address_t *nh_addr, u32 sw_if_index); diff --git a/vnet/vnet/adj/adj_nbr.c b/vnet/vnet/adj/adj_nbr.c index 1a78ecbc..003e18e8 100644 --- a/vnet/vnet/adj/adj_nbr.c +++ b/vnet/vnet/adj/adj_nbr.c @@ -76,7 +76,8 @@ adj_nbr_insert (fib_protocol_t nh_proto, } void -adj_nbr_remove (fib_protocol_t nh_proto, +adj_nbr_remove (adj_index_t ai, + fib_protocol_t nh_proto, vnet_link_t link_type, const ip46_address_t *nh_addr, u32 sw_if_index) @@ -87,6 +88,7 @@ adj_nbr_remove (fib_protocol_t nh_proto, return; ADJ_NBR_SET_KEY(kv, link_type, nh_addr); + kv.value = ai; BV(clib_bihash_add_del) (adj_nbr_tables[nh_proto][sw_if_index], &kv, 0); } diff --git a/vnet/vnet/ethernet/arp.c b/vnet/vnet/ethernet/arp.c index eeaac4d3..c6dbbc68 100644 --- a/vnet/vnet/ethernet/arp.c +++ b/vnet/vnet/ethernet/arp.c @@ -1509,7 +1509,7 @@ arp_add_del_interface_address (ip4_main_t * im, ethernet_arp_main_t *am = ðernet_arp_main; ethernet_arp_ip4_entry_t *e; - if (vec_len (am->ethernet_arp_by_sw_if_index) < sw_if_index) + if (vec_len (am->ethernet_arp_by_sw_if_index) <= sw_if_index) return; if (is_del) diff --git a/vnet/vnet/gre/gre.c b/vnet/vnet/gre/gre.c index a4b3f9fc..0faed13e 100644 --- a/vnet/vnet/gre/gre.c +++ b/vnet/vnet/gre/gre.c @@ -250,9 +250,9 @@ gre_update_adj (vnet_main_t * vnm, * @brief TX function. Only called L2. L3 traffic uses the adj-midchains */ static uword -gre_interface_tx (vlib_main_t * vm, - vlib_node_runtime_t * node, - vlib_frame_t * frame) +gre_interface_tx_inline (vlib_main_t * vm, + vlib_node_runtime_t * node, + vlib_frame_t * frame) { gre_main_t * gm = &gre_main; u32 next_index; @@ -318,12 +318,34 @@ gre_interface_tx (vlib_main_t * vm, return frame->n_vectors; } +static uword +gre_interface_tx (vlib_main_t * vm, + vlib_node_runtime_t * node, + vlib_frame_t * frame) +{ + return (gre_interface_tx_inline (vm, node, frame)); +} + +static uword +gre_teb_interface_tx (vlib_main_t * vm, + vlib_node_runtime_t * node, + vlib_frame_t * frame) +{ + return (gre_interface_tx_inline (vm, node, frame)); +} + static u8 * format_gre_tunnel_name (u8 * s, va_list * args) { u32 dev_instance = va_arg (*args, u32); return format (s, "gre%d", dev_instance); } +static u8 * format_gre_tunnel_teb_name (u8 * s, va_list * args) +{ + u32 dev_instance = va_arg (*args, u32); + return format (s, "teb-gre%d", dev_instance); +} + static u8 * format_gre_device (u8 * s, va_list * args) { u32 dev_instance = va_arg (*args, u32); @@ -348,6 +370,21 @@ VNET_DEVICE_CLASS (gre_device_class) = { VLIB_DEVICE_TX_FUNCTION_MULTIARCH (gre_device_class, gre_interface_tx) +VNET_DEVICE_CLASS (gre_device_teb_class) = { + .name = "GRE TEB tunnel device", + .format_device_name = format_gre_tunnel_teb_name, + .format_device = format_gre_device, + .format_tx_trace = format_gre_tx_trace, + .tx_function = gre_teb_interface_tx, + .admin_up_down_function = gre_interface_admin_up_down, +#ifdef SOON + .clear counter = 0; +#endif +}; + +VLIB_DEVICE_TX_FUNCTION_MULTIARCH (gre_device_teb_class, + gre_teb_interface_tx) + VNET_HW_INTERFACE_CLASS (gre_hw_interface_class) = { .name = "GRE", .format_header = format_gre_header_with_length, diff --git a/vnet/vnet/gre/gre.h b/vnet/vnet/gre/gre.h index a0ee9ad2..b6544b9b 100644 --- a/vnet/vnet/gre/gre.h +++ b/vnet/vnet/gre/gre.h @@ -36,20 +36,48 @@ typedef enum { GRE_N_ERROR, } gre_error_t; +/** + * A GRE payload protocol registration + */ typedef struct { - /* Name (a c string). */ + /** Name (a c string). */ char * name; - /* GRE protocol type in host byte order. */ + /** GRE protocol type in host byte order. */ gre_protocol_t protocol; - /* Node which handles this type. */ + /** Node which handles this type. */ u32 node_index; - /* Next index for this type. */ + /** Next index for this type. */ u32 next_index; } gre_protocol_info_t; +/** + * @brief The GRE tunnel type + */ +typedef enum gre_tunnel_tyoe_t_ +{ + /** + * L3 GRE (i.e. this tunnel is in L3 mode) + */ + GRE_TUNNEL_TYPE_L3, + /** + * Transparent Ethernet Bridging - the tunnel is in L2 mode + */ + GRE_TUNNEL_TYPE_TEB, +} gre_tunnel_type_t; + +#define GRE_TUNNEL_TYPE_NAMES { \ + [GRE_TUNNEL_TYPE_L3] = "L3", \ + [GRE_TUNNEL_TYPE_TEB] = "TEB", \ +} + +#define GRE_TUNNEL_N_TYPES ((gre_tunnel_type_t)GRE_TUNNEL_TYPE_TEB+1) + +/** + * @brief A representation of a GRE tunnel + */ typedef struct { /** * Linkage into the FIB object graph @@ -70,7 +98,7 @@ typedef struct { u32 outer_fib_index; u32 hw_if_index; u32 sw_if_index; - u8 teb; + gre_tunnel_type_t type; /** * The FIB entry sourced by the tunnel for its destination prefix @@ -96,21 +124,39 @@ typedef struct { adj_index_t l2_adj_index; } gre_tunnel_t; +/** + * @brief GRE related global data + */ typedef struct { - /* pool of tunnel instances */ + /** + * pool of tunnel instances + */ gre_tunnel_t *tunnels; + /** + * GRE payload protocol registrations + */ gre_protocol_info_t * protocol_infos; - /* Hash tables mapping name/protocol to protocol info index. */ + /** + * Hash tables mapping name/protocol to protocol info index. + */ uword * protocol_info_by_name, * protocol_info_by_protocol; - /* Hash mapping src/dst addr pair to tunnel */ + /** + * Hash mapping src/dst addr pair to tunnel + */ uword * tunnel_by_key; - /* Free vlib hw_if_indices */ - u32 * free_gre_tunnel_hw_if_indices; + /** + * Free vlib hw_if_indices. + * A free list per-tunnel type since the interfaces ctreated are fo different + * types and we cannot change the type. + */ + u32 * free_gre_tunnel_hw_if_indices[GRE_TUNNEL_N_TYPES]; - /* Mapping from sw_if_index to tunnel index */ + /** + * Mapping from sw_if_index to tunnel index + */ u32 * tunnel_index_by_sw_if_index; /* convenience */ @@ -120,8 +166,7 @@ typedef struct { /** * @brief IPv4 and GRE header. - * -*/ + */ typedef CLIB_PACKED (struct { ip4_header_t ip4; gre_header_t gre; @@ -148,8 +193,8 @@ extern clib_error_t * gre_interface_admin_up_down (vnet_main_t * vnm, extern void gre_tunnel_stack (adj_index_t ai); extern void gre_update_adj (vnet_main_t * vnm, - u32 sw_if_index, - adj_index_t ai); + u32 sw_if_index, + adj_index_t ai); format_function_t format_gre_protocol; format_function_t format_gre_header; @@ -157,7 +202,7 @@ format_function_t format_gre_header_with_length; extern vlib_node_registration_t gre_input_node; extern vnet_device_class_t gre_device_class; -extern vnet_device_class_t gre_l2_device_class; +extern vnet_device_class_t gre_device_teb_class; /* Parse gre protocol as 0xXXXX or protocol name. In either host or network byte order. */ diff --git a/vnet/vnet/gre/interface.c b/vnet/vnet/gre/interface.c index 3234de09..7adc5268 100644 --- a/vnet/vnet/gre/interface.c +++ b/vnet/vnet/gre/interface.c @@ -24,6 +24,8 @@ #include #include +static const char *gre_tunnel_type_names[] = GRE_TUNNEL_TYPE_NAMES; + static inline u64 gre_mk_key (const ip4_address_t *src, const ip4_address_t *dst, @@ -33,6 +35,14 @@ gre_mk_key (const ip4_address_t *src, return ((u64)src->as_u32 << 32 | (u64)dst->as_u32); } +static u8 * +format_gre_tunnel_type (u8 * s, va_list * args) +{ + gre_tunnel_type_t type = va_arg (*args, gre_tunnel_type_t); + + return (format(s, "%s", gre_tunnel_type_names[type])); +} + static u8 * format_gre_tunnel (u8 * s, va_list * args) { @@ -40,11 +50,11 @@ format_gre_tunnel (u8 * s, va_list * args) gre_main_t * gm = &gre_main; s = format (s, - "[%d] %U (src) %U (dst) payload %s outer_fib_index %d", + "[%d] %U (src) %U (dst) payload %U outer_fib_index %d", t - gm->tunnels, format_ip4_address, &t->tunnel_src, format_ip4_address, &t->tunnel_dst, - (t->teb ? "teb" : "ip"), + format_gre_tunnel_type, t->type, t->outer_fib_index); return s; @@ -248,12 +258,17 @@ vnet_gre_tunnel_add (vnet_gre_add_del_tunnel_args_t *a, memset (t, 0, sizeof (*t)); fib_node_init(&t->node, FIB_NODE_TYPE_GRE_TUNNEL); - if (vec_len (gm->free_gre_tunnel_hw_if_indices) > 0) { + if (a->teb) + t->type = GRE_TUNNEL_TYPE_TEB; + else + t->type = GRE_TUNNEL_TYPE_L3; + + if (vec_len (gm->free_gre_tunnel_hw_if_indices[t->type]) > 0) { vnet_interface_main_t * im = &vnm->interface_main; - hw_if_index = gm->free_gre_tunnel_hw_if_indices - [vec_len (gm->free_gre_tunnel_hw_if_indices)-1]; - _vec_len (gm->free_gre_tunnel_hw_if_indices) -= 1; + hw_if_index = gm->free_gre_tunnel_hw_if_indices[t->type] + [vec_len (gm->free_gre_tunnel_hw_if_indices[t->type])-1]; + _vec_len (gm->free_gre_tunnel_hw_if_indices[t->type]) -= 1; hi = vnet_get_hw_interface (vnm, hw_if_index); hi->dev_instance = t - gm->tunnels; @@ -269,14 +284,14 @@ vnet_gre_tunnel_add (vnet_gre_add_del_tunnel_args_t *a, vlib_zero_simple_counter (&im->sw_if_counters[VNET_INTERFACE_COUNTER_DROP], sw_if_index); vnet_interface_counter_unlock(im); - if (a->teb) + if (GRE_TUNNEL_TYPE_TEB == t->type) { - t->l2_tx_arc = vlib_node_add_named_next(vlib_get_main(), - hi->tx_node_index, - "adj-l2-midchain"); + t->l2_tx_arc = vlib_node_add_named_next(vlib_get_main(), + hi->tx_node_index, + "adj-l2-midchain"); } } else { - if (a->teb) + if (GRE_TUNNEL_TYPE_TEB == t->type) { /* Default MAC address (d00b:eed0:0000 + sw_if_index) */ memset (address, 0, sizeof (address)); @@ -287,7 +302,7 @@ vnet_gre_tunnel_add (vnet_gre_add_del_tunnel_args_t *a, address[4] = t - gm->tunnels; error = ethernet_register_interface(vnm, - gre_device_class.index, + gre_device_teb_class.index, t - gm->tunnels, address, &hw_if_index, 0); @@ -316,14 +331,11 @@ vnet_gre_tunnel_add (vnet_gre_add_del_tunnel_args_t *a, t->hw_if_index = hw_if_index; t->outer_fib_index = outer_fib_index; t->sw_if_index = sw_if_index; - t->teb = a->teb; vec_validate_init_empty (gm->tunnel_index_by_sw_if_index, sw_if_index, ~0); gm->tunnel_index_by_sw_if_index[sw_if_index] = t - gm->tunnels; vec_validate (im->fib_index_by_sw_if_index, sw_if_index); - im->fib_index_by_sw_if_index[sw_if_index] = t->outer_fib_index; - ip4_sw_interface_enable_disable(sw_if_index, 1); hi->min_packet_bytes = 64 + sizeof (gre_header_t) + sizeof (ip4_header_t); hi->per_packet_overhead_bytes = @@ -365,13 +377,12 @@ vnet_gre_tunnel_add (vnet_gre_add_del_tunnel_args_t *a, clib_memcpy (&t->tunnel_src, &a->src, sizeof (t->tunnel_src)); clib_memcpy (&t->tunnel_dst, &a->dst, sizeof (t->tunnel_dst)); - if (t->teb) + if (GRE_TUNNEL_TYPE_TEB == t->type) { t->l2_adj_index = adj_nbr_add_or_lock(FIB_PROTOCOL_IP4, VNET_LINK_ETHERNET, &zero_addr, sw_if_index); - gre_update_adj(vnm, t->sw_if_index, t->l2_adj_index); } @@ -399,9 +410,11 @@ vnet_gre_tunnel_delete (vnet_gre_add_del_tunnel_args_t *a, vnet_sw_interface_set_flags (vnm, sw_if_index, 0 /* down */); /* make sure tunnel is removed from l2 bd or xconnect */ set_int_l2_mode(gm->vlib_main, vnm, MODE_L3, sw_if_index, 0, 0, 0, 0); - vec_add1 (gm->free_gre_tunnel_hw_if_indices, t->hw_if_index); + vec_add1 (gm->free_gre_tunnel_hw_if_indices[t->type], t->hw_if_index); gm->tunnel_index_by_sw_if_index[sw_if_index] = ~0; - ip4_sw_interface_enable_disable(sw_if_index, 0); + + if (GRE_TUNNEL_TYPE_TEB == t->type) + adj_unlock(t->l2_adj_index); fib_entry_child_remove(t->fib_entry_index, t->sibling_index); diff --git a/vnet/vnet/gre/node.c b/vnet/vnet/gre/node.c index 556f1a81..86f7a6ee 100644 --- a/vnet/vnet/gre/node.c +++ b/vnet/vnet/gre/node.c @@ -68,12 +68,10 @@ gre_input (vlib_main_t * vm, vlib_frame_t * from_frame) { gre_main_t * gm = &gre_main; - ip4_main_t * ip4m = &ip4_main; gre_input_runtime_t * rt = (void *) node->runtime_data; __attribute__((unused)) u32 n_left_from, next_index, * from, * to_next; u64 cached_tunnel_key = (u64) ~0; u32 cached_tunnel_sw_if_index = 0, tunnel_sw_if_index = 0; - u32 cached_tunnel_fib_index = 0, tunnel_fib_index; u32 cpu_index = os_get_cpu_number(); u32 len; @@ -193,16 +191,12 @@ gre_input (vlib_main_t * vm, hi = vnet_get_hw_interface (gm->vnet_main, t->hw_if_index); tunnel_sw_if_index = hi->sw_if_index; - tunnel_fib_index = vec_elt (ip4m->fib_index_by_sw_if_index, - tunnel_sw_if_index); cached_tunnel_sw_if_index = tunnel_sw_if_index; - cached_tunnel_fib_index = tunnel_fib_index; } else { tunnel_sw_if_index = cached_tunnel_sw_if_index; - tunnel_fib_index = cached_tunnel_fib_index; } } else @@ -218,7 +212,6 @@ gre_input (vlib_main_t * vm, 1 /* packets */, len /* bytes */); - vnet_buffer(b0)->sw_if_index[VLIB_TX] = tunnel_fib_index; vnet_buffer(b0)->sw_if_index[VLIB_RX] = tunnel_sw_if_index; drop0: @@ -247,16 +240,12 @@ drop0: hi = vnet_get_hw_interface (gm->vnet_main, t->hw_if_index); tunnel_sw_if_index = hi->sw_if_index; - tunnel_fib_index = vec_elt (ip4m->fib_index_by_sw_if_index, - tunnel_sw_if_index); cached_tunnel_sw_if_index = tunnel_sw_if_index; - cached_tunnel_fib_index = tunnel_fib_index; } else { tunnel_sw_if_index = cached_tunnel_sw_if_index; - tunnel_fib_index = cached_tunnel_fib_index; } } else @@ -272,7 +261,6 @@ drop0: 1 /* packets */, len /* bytes */); - vnet_buffer(b1)->sw_if_index[VLIB_TX] = tunnel_fib_index; vnet_buffer(b1)->sw_if_index[VLIB_RX] = tunnel_sw_if_index; drop1: @@ -280,7 +268,7 @@ drop1: { gre_rx_trace_t *tr = vlib_add_trace (vm, node, b0, sizeof (*tr)); - tr->tunnel_id = ~0; + tr->tunnel_id = tunnel_sw_if_index; tr->length = ip0->length; tr->src.as_u32 = ip0->src_address.as_u32; tr->dst.as_u32 = ip0->dst_address.as_u32; @@ -290,7 +278,7 @@ drop1: { gre_rx_trace_t *tr = vlib_add_trace (vm, node, b1, sizeof (*tr)); - tr->tunnel_id = ~0; + tr->tunnel_id = tunnel_sw_if_index; tr->length = ip1->length; tr->src.as_u32 = ip1->src_address.as_u32; tr->dst.as_u32 = ip1->dst_address.as_u32; @@ -374,16 +362,12 @@ drop1: hi = vnet_get_hw_interface (gm->vnet_main, t->hw_if_index); tunnel_sw_if_index = hi->sw_if_index; - tunnel_fib_index = vec_elt (ip4m->fib_index_by_sw_if_index, - tunnel_sw_if_index); cached_tunnel_sw_if_index = tunnel_sw_if_index; - cached_tunnel_fib_index = tunnel_fib_index; } else { tunnel_sw_if_index = cached_tunnel_sw_if_index; - tunnel_fib_index = cached_tunnel_fib_index; } } else @@ -399,7 +383,6 @@ drop1: 1 /* packets */, len /* bytes */); - vnet_buffer(b0)->sw_if_index[VLIB_TX] = tunnel_fib_index; vnet_buffer(b0)->sw_if_index[VLIB_RX] = tunnel_sw_if_index; drop: diff --git a/vnet/vnet/interface_output.c b/vnet/vnet/interface_output.c index 1d1546f6..46e8a98c 100644 --- a/vnet/vnet/interface_output.c +++ b/vnet/vnet/interface_output.c @@ -58,15 +58,30 @@ format_vnet_interface_output_trace (u8 * s, va_list * va) if (t->sw_if_index != (u32) ~ 0) { - si = vnet_get_sw_interface (vnm, t->sw_if_index); indent = format_get_indent (s); - s = format (s, "%U\n%U%U", - format_vnet_sw_interface_name, vnm, si, - format_white_space, indent, - node->format_buffer ? node-> - format_buffer : format_hex_bytes, t->data, - sizeof (t->data)); + if (pool_is_free_index + (vnm->interface_main.sw_interfaces, t->sw_if_index)) + { + /* the interface may have been deleted by the time the trace is printed */ + s = format (s, "sw_if_index: %d\n%U%U", + t->sw_if_index, + format_white_space, indent, + node->format_buffer ? node-> + format_buffer : format_hex_bytes, t->data, + sizeof (t->data)); + } + else + { + si = vnet_get_sw_interface (vnm, t->sw_if_index); + + s = format (s, "%U\n%U%U", + format_vnet_sw_interface_name, vnm, si, + format_white_space, indent, + node->format_buffer ? node-> + format_buffer : format_hex_bytes, t->data, + sizeof (t->data)); + } } return s; } diff --git a/vnet/vnet/ip/ip6_forward.c b/vnet/vnet/ip/ip6_forward.c index bc346786..14dd9dfb 100644 --- a/vnet/vnet/ip/ip6_forward.c +++ b/vnet/vnet/ip/ip6_forward.c @@ -498,6 +498,8 @@ ip6_add_del_interface_address (vlib_main_t * vm, goto done; } + ip6_sw_interface_enable_disable(sw_if_index, !is_del); + if (is_del) ip6_del_interface_routes (im, ip6_af.fib_index, address, address_length); diff --git a/vnet/vnet/ip/ip6_neighbor.c b/vnet/vnet/ip/ip6_neighbor.c index cebe09a9..af852a2b 100644 --- a/vnet/vnet/ip/ip6_neighbor.c +++ b/vnet/vnet/ip/ip6_neighbor.c @@ -1849,7 +1849,6 @@ ip6_neighbor_sw_interface_add_del (vnet_main_t * vnm, pool_put (nm->if_radv_pool, a); nm->if_radv_pool_index_by_sw_if_index[sw_if_index] = ~0; ri = ~0; - ip6_sw_interface_enable_disable(sw_if_index, 0); } } else @@ -1858,7 +1857,6 @@ ip6_neighbor_sw_interface_add_del (vnet_main_t * vnm, { vnet_hw_interface_t * hw_if0; - ip6_sw_interface_enable_disable(sw_if_index, 1); hw_if0 = vnet_get_sup_hw_interface (vnm, sw_if_index); pool_get (nm->if_radv_pool, a); diff --git a/vpp/vpp-api/api.c b/vpp/vpp-api/api.c index 91726e16..676b0c45 100644 --- a/vpp/vpp-api/api.c +++ b/vpp/vpp-api/api.c @@ -5008,7 +5008,7 @@ static void send_gre_tunnel_details clib_memcpy (rmp->src_address, &(t->tunnel_src), 4); clib_memcpy (rmp->dst_address, &(t->tunnel_dst), 4); rmp->outer_fib_id = htonl (im->fibs[t->outer_fib_index].ft_table_id); - rmp->teb = t->teb; + rmp->teb = (GRE_TUNNEL_TYPE_TEB == t->type); rmp->sw_if_index = htonl (t->sw_if_index); rmp->context = context; -- cgit 1.2.3-korg From 0279b29f327c15a1c6b2d8ede228790c1a7d3814 Mon Sep 17 00:00:00 2001 From: Juraj Sloboda Date: Wed, 16 Nov 2016 19:50:24 +0100 Subject: Add make test code coverage reporting using gcov Change-Id: Ia8247841fdbe76e1d888aab49ae213b4216af273 Signed-off-by: Juraj Sloboda --- .gitignore | 1 + Makefile | 15 +++++++++++++-- build-data/platforms/vpp_lite.mk | 5 +++++ test/Makefile | 29 +++++++++++++++++++++++++---- 4 files changed, 44 insertions(+), 6 deletions(-) (limited to 'test/Makefile') diff --git a/.gitignore b/.gitignore index cec432c5..c12eedfb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /build-root/*.rpm /build-root/*.changes /build-root/test-doc/ +/build-root/test-cov/ /build-root/python/ /build-config.mk /dpdk/*.tar.gz diff --git a/Makefile b/Makefile index 00699a76..50155664 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ endif DEB_DEPENDS = curl build-essential autoconf automake bison libssl-dev ccache DEB_DEPENDS += debhelper dkms git libtool libganglia1-dev libapr1-dev dh-systemd DEB_DEPENDS += libconfuse-dev git-review exuberant-ctags cscope -DEB_DEPENDS += python-dev python-virtualenv python-pip +DEB_DEPENDS += python-dev python-virtualenv python-pip lcov ifeq ($(OS_VERSION_ID),14.04) DEB_DEPENDS += openjdk-8-jdk-headless else @@ -43,7 +43,7 @@ endif RPM_DEPENDS_GROUPS = 'Development Tools' RPM_DEPENDS = redhat-lsb glibc-static java-1.8.0-openjdk-devel yum-utils RPM_DEPENDS += openssl-devel https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm apr-devel -RPM_DEPENDS += python-devel python-virtualenv +RPM_DEPENDS += python-devel python-virtualenv lcov EPEL_DEPENDS = libconfuse-devel ganglia-devel ifneq ($(wildcard $(STARTUP_DIR)/startup.conf),) @@ -59,6 +59,7 @@ endif .PHONY: run run-release debug debug-release build-vat run-vat pkg-deb pkg-rpm .PHONY: ctags cscope plugins plugins-release build-vpp-api .PHONY: test test-debug retest retest-debug test-doc test-wipe-doc test-help test-wipe +.PHONY: test-cov test-wipe-cov help: @echo "Make Targets:" @@ -97,6 +98,8 @@ help: @echo " wipe-doxygen - wipe all generated documentation" @echo " test-doc - generate documentation for test framework" @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 "" @echo "Make Arguments:" @echo " V=[0|1] - set build verbosity level" @@ -219,6 +222,8 @@ VPP_PYTHON_PREFIX=$(BR)/python define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install,) make -C test \ + BR=$(BR) \ + VPP_TEST_BUILD_DIR=$(BR)/build-$(2)-native \ 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 \ @@ -245,6 +250,12 @@ test-doc: test-wipe-doc: @make -C test wipe-doc BR=$(BR) +test-cov: + $(call test,vpp_lite,vpp_lite_gcov,cov) + +test-wipe-cov: + @make -C test wipe-cov BR=$(BR) + retest: $(call test,vpp_lite,vpp_lite,retest) diff --git a/build-data/platforms/vpp_lite.mk b/build-data/platforms/vpp_lite.mk index 94e69511..ef2ec444 100644 --- a/build-data/platforms/vpp_lite.mk +++ b/build-data/platforms/vpp_lite.mk @@ -44,3 +44,8 @@ vpp_lite_TAG_CFLAGS = -g -O2 -DFORTIFY_SOURCE=2 -march=$(MARCH) -mtune=$(MTUNE) -fstack-protector -fPIC -Werror vpp_lite_TAG_LDFLAGS = -g -O2 -DFORTIFY_SOURCE=2 -march=$(MARCH) -mtune=$(MTUNE) \ -fstack-protector -fPIC -Werror + +vpp_lite_gcov_TAG_CFLAGS = -g -O0 -DCLIB_DEBUG -march=$(MARCH) \ + -fPIC -Werror -fprofile-arcs -ftest-coverage +vpp_lite_gcov_TAG_LDFLAGS = -g -O0 -DCLIB_DEBUG -march=$(MARCH) \ + -fPIC -Werror -coverage diff --git a/test/Makefile b/test/Makefile index de6aaa7a..7561a009 100644 --- a/test/Makefile +++ b/test/Makefile @@ -8,6 +8,7 @@ endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv PYTHON_DEPENDS=scapy==2.3.3 pexpect SCAPY_SOURCE=$(WS_ROOT)/build-root/python/virtualenv/lib/python2.7/site-packages/ +BUILD_COV_DIR = $(BR)/test-cov .pip-install.ok: @@ -30,11 +31,15 @@ SCAPY_SOURCE=$(WS_ROOT)/build-root/python/virtualenv/lib/python2.7/site-packages PHONIES=.install.ok .pip-patch.ok .pip-install.ok .PHONY: $(PHONIES) -test: reset verify-python-path .install.ok +define retest-func @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" +endef + +test: reset verify-python-path .install.ok + $(call retest-func) retest: reset verify-python-path - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" + $(call retest-func) .PHONY: wipe doc @@ -53,6 +58,18 @@ doc: verify-python-path wipe-doc: @make -C doc wipe BR=$(BR) +cov: wipe-cov reset verify-python-path .install.ok + @lcov --zerocounters --directory $(VPP_TEST_BUILD_DIR) + $(call retest-func) + @mkdir $(BUILD_COV_DIR) + @lcov --capture --directory $(VPP_TEST_BUILD_DIR) --output-file $(BUILD_COV_DIR)/coverage.info + @genhtml $(BUILD_COV_DIR)/coverage.info --output-directory $(BUILD_COV_DIR)/html + @echo + @echo "Build finished. Code coverage report is in $(BUILD_COV_DIR)/html/index.html" + +wipe-cov: wipe + @rm -rf $(BUILD_COV_DIR) + help: @echo "Running tests:" @echo "" @@ -60,7 +77,7 @@ help: @echo " test-debug - build and run functional tests (debug build)" @echo " retest - run functional tests" @echo " retest-debug - run functional tests (debug build)" - @echo " wipe-test - wipe (temporary) files generated by unit tests" + @echo " test-wipe - wipe (temporary) files generated by unit tests" @echo "" @echo "Arguments controlling test runs:" @echo " V=[0|1|2] - set test verbosity level" @@ -76,5 +93,9 @@ help: @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" - @echo " wipe-test-doc - wipe documentation for test framework" + @echo " test-wipe-doc - wipe documentation for test framework" + @echo "" + @echo "Creating test code coverage report" + @echo " test-cov - generate code coverage report for test framework" + @echo " test-wipe-cov - wipe code coverage report for test framework" @echo "" -- cgit 1.2.3-korg From 9f9d07b801e2e82dcfc7c06527e11f593d6a9f9a Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Mon, 12 Dec 2016 09:20:50 +0100 Subject: make test: improve virtualenv patching process Change-Id: I5041bdc733c4da633d83f60f7db7747b4d0b8894 Signed-off-by: Klement Sekera --- test/Makefile | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 7561a009..54fe0582 100644 --- a/test/Makefile +++ b/test/Makefile @@ -7,16 +7,21 @@ endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv PYTHON_DEPENDS=scapy==2.3.3 pexpect -SCAPY_SOURCE=$(WS_ROOT)/build-root/python/virtualenv/lib/python2.7/site-packages/ +SCAPY_SOURCE=$(PYTHON_VENV_PATH)/lib/python2.7/site-packages/ BUILD_COV_DIR = $(BR)/test-cov +PIP_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/pip-install.done +PIP_PATCH_DONE=$(VPP_PYTHON_PREFIX)/pip-patch.done +PAPI_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/papi-install.done -.pip-install.ok: +PAPI_INSTALL_FLAGS=$(PIP_INSTALL_DONE) $(PIP_PATCH_DONE) $(PAPI_INSTALL_DONE) + +$(PIP_INSTALL_DONE): @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS)" @touch $@ -.pip-patch.ok: .pip-install.ok +$(PIP_PATCH_DONE): $(PIP_INSTALL_DONE) @echo --- patching --- for f in $(CURDIR)/patches/scapy-2.3.3/*.patch ; do \ echo Applying patch: $$(basename $$f) ; \ @@ -24,18 +29,15 @@ BUILD_COV_DIR = $(BR)/test-cov done @touch $@ -.install.ok: .pip-patch.ok +$(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && cd $(WS_ROOT)/vpp-api/python && python setup.py install" @touch $@ -PHONIES=.install.ok .pip-patch.ok .pip-install.ok -.PHONY: $(PHONIES) - define retest-func @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" endef -test: reset verify-python-path .install.ok +test: reset verify-python-path $(PAPI_INSTALL_DONE) $(call retest-func) retest: reset verify-python-path @@ -48,17 +50,20 @@ reset: @rm -rf /tmp/vpp-unittest-* wipe: reset - @rm -f $(PHONIES) + @rm -rf $(PYTHON_VENV_PATH) + @rm -f $(PAPI_INSTALL_FLAGS) doc: verify-python-path @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" +.PHONY: wipe-doc + wipe-doc: @make -C doc wipe BR=$(BR) -cov: wipe-cov reset verify-python-path .install.ok +cov: wipe-cov reset verify-python-path $(PAPI_INSTALL_DONE) @lcov --zerocounters --directory $(VPP_TEST_BUILD_DIR) $(call retest-func) @mkdir $(BUILD_COV_DIR) @@ -67,6 +72,8 @@ cov: wipe-cov reset verify-python-path .install.ok @echo @echo "Build finished. Code coverage report is in $(BUILD_COV_DIR)/html/index.html" +.PHONY: wipe-cov + wipe-cov: wipe @rm -rf $(BUILD_COV_DIR) -- cgit 1.2.3-korg From 37d77e9edbf845de23d41c343c11b34a575761ff Mon Sep 17 00:00:00 2001 From: Damjan Marion Date: Wed, 28 Dec 2016 15:14:46 +0100 Subject: test: fix paths after source code move Change-Id: I3100260bb697f9af2f73ad3777e8b27069c5babe Signed-off-by: Damjan Marion --- Makefile | 5 ++--- test/Makefile | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 1b75779a..eeb9837a 100644 --- a/Makefile +++ b/Makefile @@ -223,10 +223,9 @@ define test BR=$(BR) \ VPP_TEST_BUILD_DIR=$(BR)/build-$(2)-native \ 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 \ + VPP_TEST_PLUGIN_PATH=$(BR)/install-$(2)-native/vpp/lib64/vpp_plugins \ VPP_TEST_INSTALL_PATH=$(BR)/install-$(2)-native/ \ - LD_LIBRARY_PATH=$(BR)/install-$(2)-native/vpp-api/lib64/ \ + LD_LIBRARY_PATH=$(BR)/install-$(2)-native/vpp/lib64/ \ WS_ROOT=$(WS_ROOT) V=$(V) TEST=$(TEST) VPP_PYTHON_PREFIX=$(VPP_PYTHON_PREFIX) $(3) endef diff --git a/test/Makefile b/test/Makefile index 54fe0582..204afd38 100644 --- a/test/Makefile +++ b/test/Makefile @@ -30,7 +30,7 @@ $(PIP_PATCH_DONE): $(PIP_INSTALL_DONE) @touch $@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) - @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 && cd $(WS_ROOT)/src/vpp-api/python && python setup.py install" @touch $@ define retest-func -- cgit 1.2.3-korg From da505f608e0919c45089dc80f9e3e16330a6551a Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Wed, 4 Jan 2017 12:58:53 +0100 Subject: make test: improve documentation and PEP8 compliance Change-Id: Ib4f0353aab6112fcc3c3d8f0bcbed5bc4b567b9b Signed-off-by: Klement Sekera --- test/Makefile | 4 +- test/doc/index.rst | 285 ++++++++++++++++++++++++++++++++++-- test/framework.py | 23 +-- test/hook.py | 4 +- test/template_bd.py | 4 +- test/test_classifier.py | 14 +- test/test_flowperpkt.py | 4 +- test/test_ip4.py | 17 ++- test/test_ip4_vrf_multi_instance.py | 11 +- test/test_ip6.py | 57 ++++---- test/test_l2_fib.py | 11 +- test/test_l2bd_multi_instance.py | 13 +- test/test_l2xc_multi_instance.py | 20 ++- test/test_snat.py | 20 ++- test/test_span.py | 11 +- test/vpp_interface.py | 14 +- test/vpp_ip_route.py | 52 ++++--- test/vpp_papi_provider.py | 12 +- test/vpp_pg_interface.py | 48 +++--- 19 files changed, 468 insertions(+), 156 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 204afd38..543fe913 100644 --- a/test/Makefile +++ b/test/Makefile @@ -17,7 +17,7 @@ PAPI_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/papi-install.done PAPI_INSTALL_FLAGS=$(PIP_INSTALL_DONE) $(PIP_PATCH_DONE) $(PAPI_INSTALL_DONE) $(PIP_INSTALL_DONE): - @virtualenv $(PYTHON_VENV_PATH) + @virtualenv $(PYTHON_VENV_PATH) -p python2.7 @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS)" @touch $@ @@ -54,7 +54,7 @@ wipe: reset @rm -f $(PAPI_INSTALL_FLAGS) doc: verify-python-path - @virtualenv $(PYTHON_VENV_PATH) + @virtualenv $(PYTHON_VENV_PATH) -p python2.7 @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" diff --git a/test/doc/index.rst b/test/doc/index.rst index 8cbe961f..f51d5058 100644 --- a/test/doc/index.rst +++ b/test/doc/index.rst @@ -4,6 +4,7 @@ .. _SkipTest: https://docs.python.org/2/library/unittest.html#unittest.SkipTest .. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/ .. _scapy: http://www.secdev.org/projects/scapy/ +.. _logging: https://docs.python.org/2/library/logging.html .. |vtf| replace:: VPP Test Framework @@ -39,7 +40,7 @@ Function flow when running a test case is: 3. *test_*: This is the guts of the test case. It should execute the test scenario and use the various assert functions from the unittest framework to check - necessary. + necessary. Multiple test_ methods can exist in a test case. 4. `tearDown `: The tearDown function is called after each test function with the purpose of doing partial cleanup. @@ -47,16 +48,56 @@ Function flow when running a test case is: Method called once after running all of the test functions to perform the final cleanup. +Logging +####### + +Each test case has a logger automatically created for it, stored in +'logger' property, based on logging_. Use the logger's standard methods +debug(), info(), error(), ... to emit log messages to the logger. + +All the log messages go always into a log file in temporary directory +(see below). + +To control the messages printed to console, specify the V= parameter. + +.. code-block:: shell + + make test # minimum verbosity + make test V=1 # moderate verbosity + make test V=2 # maximum verbosity + Test temporary directory and VPP life cycle ########################################### Test separation is achieved by separating the test files and vpp instances. Each test creates a temporary directory and it's name is used to create a shared memory prefix which is used to run a VPP instance. +The temporary directory name contains the testcase class name for easy +reference, so for testcase named 'TestVxlan' the directory could be named +e.g. vpp-unittest-TestVxlan-UNUP3j. This way, there is no conflict between any other VPP instances running on the box and the test VPP. Any temporary files created by the test case are stored in this temporary test directory. +The test temporary directory holds the following interesting files: + +* log.txt - this contains the logger output on max verbosity +* pg*_in.pcap - last injected packet stream into VPP, named after the interface, + so for pg0, the file will be named pg0_in.pcap +* pg*_out.pcap - last capture file created by VPP for interface, similarly, + named after the interface, so for e.g. pg1, the file will be named + pg1_out.pcap +* history files - whenever the capture is restarted or a new stream is added, + the existing files are rotated and renamed, soo all the pcap files + are always saved for later debugging if needed +* core - if vpp dumps a core, it'll be stored in the temporary directory +* vpp_stdout.txt - file containing output which vpp printed to stdout +* vpp_stderr.txt - file containing output which vpp printed to stderr + +*NOTE*: existing temporary directories named vpp-unittest-* are automatically +removed when invoking 'make test*' or 'make retest*' to keep the temporary +directory clean. + Virtual environment ################### @@ -82,6 +123,37 @@ thus: So e.g. `remote_mac ` address is the MAC address assigned to the virtual host connected to the VPP. +Automatically generated addresses +################################# + +To send packets, one needs to typically provide some addresses, otherwise +the packets will be dropped. The interface objects in |vtf| automatically +provide addresses based on (typically) their indexes, which ensures +there are no conflicts and eases debugging by making the addressing scheme +consistent. + +The developer of a test case typically doesn't need to work with the actual +numbers, rather using the properties of the objects. The addresses typically +come in two flavors: '
' and '
n' - note the 'n' suffix. +The former address is a Python string, while the latter is translated using +socket.inet_pton to raw format in network byte order - this format is suitable +for passing as an argument to VPP APIs. + +e.g. for the IPv4 address assigned to the VPP interface: + +* local_ip4 - Local IPv4 address on VPP interface (string) +* local_ip4n - Local IPv4 address - raw, suitable as API parameter. + +These addresses need to be configured in VPP to be usable using e.g. +`config_ip4` API. Please see the documentation to `VppInterface` for more +details. + +By default, there is one remote address of each kind created for L3: +remote_ip4 and remote_ip6. If the test needs more addresses, because it's +simulating more remote hosts, they can be generated using +`generate_remote_hosts` API and the entries for them inserted into the ARP +table using `configure_ipv4_neighbors` API. + Packet flow in the |vtf| ######################## @@ -93,6 +165,10 @@ using packet-generator interfaces, represented by the `VppPGInterface` class. Packets are written into a temporary .pcap file, which is then read by the VPP and the packets are injected into the VPP world. +To add a list of packets to an interface, call the `add_stream` method on that +interface. Once everything is prepared, call `pg_start` method to start +the packet generator on the VPP side. + VPP -> test framework ~~~~~~~~~~~~~~~~~~~~~ @@ -100,6 +176,72 @@ Similarly, VPP doesn't send any packets to |vtf| directly. Instead, packet capture feature is used to capture and write traffic to a temporary .pcap file, which is then read and analyzed by the |vtf|. +The following APIs are available to the test case for reading pcap files. + +* `get_capture`: this API is suitable for bulk & batch style of test, where + a list of packets is prepared & sent, then the received packets are read + and verified. The API needs the number of packets which are expected to + be captured (ignoring filtered packets - see below) to know when the pcap + file is completely written by the VPP. If using packet infos for verifying + packets, then the counts of the packet infos can be automatically used + by `get_capture` to get the proper count (in this case the default value + None can be supplied as expected_count or ommitted altogether). +* `wait_for_packet`: this API is suitable for interactive style of test, + e.g. when doing session management, three-way handsakes, etc. This API waits + for and returns a single packet, keeping the capture file in place + and remembering context. Repeated invocations return following packets + (or raise Exception if timeout is reached) from the same capture file + (= packets arriving on the same interface). + +*NOTE*: it is not recommended to mix these APIs unless you understand how they +work internally. None of these APIs rotate the pcap capture file, so calling +e.g. `get_capture` after `wait_for_packet` will return already read packets. +It is safe to switch from one API to another after calling `enable_capture` +as that API rotates the capture file. + +Automatic filtering of packets: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both APIs (`get_capture` and `wait_for_packet`) by default filter the packet +capture, removing known uninteresting packets from it - these are IPv6 Router +Advertisments and IPv6 Router Alerts. These packets are unsolicitated +and from the point of |vtf| are random. If a test wants to receive these +packets, it should specify either None or a custom filtering function +as the value to the 'filter_out_fn' argument. + +Common API flow for sending/receiving packets: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We will describe a simple scenario, where packets are sent from pg0 to pg1 +interface, assuming that the interfaces were created using +`create_pg_interfaces` API. + +1. Create a list of packets for pg0:: + + packet_count = 10 + packets = create_packets(src=self.pg0, dst=self.pg1, + count=packet_count) + +2. Add that list of packets to the source interface:: + + self.pg0.add_stream(packets) + +3. Enable capture on the destination interface:: + + self.pg1.enable_capture() + +4. Start the packet generator:: + + self.pg_start() + +5. Wait for capture file to appear and read it:: + + capture = self.pg1.get_capture(expected_count=packet_count) + +6. Verify packets match sent packets:: + + self.verify_capture(send=packets, captured=capture) + Test framework objects ###################### @@ -113,8 +255,8 @@ common tasks easily in the test cases. * `VppSubInterface`: VPP sub-interface abstract class, containing common functionality for e.g. `VppDot1QSubint` and `VppDot1ADSubint` classes -How VPP API/CLI is called -######################### +How VPP APIs/CLIs are called +############################ Vpp provides python bindings in a python module called vpp-papi, which the test framework installs in the virtual environment. A shim layer represented by @@ -137,19 +279,144 @@ purposes: more readable. E.g. ip_add_del_route API takes ~25 parameters, of which in the common case, only 3 are needed. +Utility methods +############### + +Some interesting utility methods are: + +* `ppp`: 'Pretty Print Packet' - returns a string containing the same output + as Scapy's packet.show() would print +* `ppc`: 'Pretty Print Capture' - returns a string containing printout of + a capture (with configurable limit on the number of packets printed from it) + using `ppp` + +*NOTE*: Do not use Scapy's packet.show() in the tests, because it prints +the output to stdout. All output should go to the logger associated with +the test case. + Example: how to add a new test ############################## -In this example, we will describe how to add a new test case which tests VPP... +In this example, we will describe how to add a new test case which tests +basic IPv4 forwarding. + +1. Add a new file called test_ip4_fwd.py in the test directory, starting + with a few imports:: + + from framework import VppTestCase + from scapy.layers.l2 import Ether + from scapy.packet import Raw + from scapy.layers.inet import IP, UDP + from random import randint + +2. Create a class inherited from the VppTestCase:: + + class IP4FwdTestCase(VppTestCase): + """ IPv4 simple forwarding test case """ + +2. Add a setUpClass function containing the setup needed for our test to run:: + + @classmethod + def setUpClass(self): + super(IP4FwdTestCase, self).setUpClass() + self.create_pg_interfaces(range(2)) # create pg0 and pg1 + for i in self.pg_interfaces: + i.admin_up() # put the interface up + i.config_ip4() # configure IPv4 address on the interface + i.resolve_arp() # resolve ARP, so that we know VPP MAC + +3. Create a helper method to create the packets to send:: + + def create_stream(self, src_if, dst_if, count): + packets = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + 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=randint(1000, 2000), dport=5678) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + packets.append(p) + + # return the created packet list + return packets + +4. Create a helper method to verify the capture:: + + def verify_capture(self, src_if, dst_if, capture): + packet_info = None + for packet in capture: + try: + ip = packet[IP] + udp = packet[UDP] + # convert the payload to packet info object + payload_info = self.payload_to_info(str(packet[Raw])) + # make sure the indexes match + self.assert_equal(payload_info.src, src_if.sw_if_index, + "source sw_if_index") + self.assert_equal(payload_info.dst, dst_if.sw_if_index, + "destination sw_if_index") + packet_info = self.get_next_packet_info_for_interface2( + src_if.sw_if_index, + dst_if.sw_if_index, + packet_info) + # make sure we didn't run out of saved packets + self.assertIsNotNone(packet_info) + self.assert_equal(payload_info.index, packet_info.index, + "packet info index") + saved_packet = packet_info.data # fetch the saved packet + # assert the values match + self.assert_equal(ip.src, saved_packet[IP].src, + "IP source address") + # ... more assertions here + self.assert_equal(udp.sport, saved_packet[UDP].sport, + "UDP source port") + except: + self.logger.error(ppp("Unexpected or invalid packet:", + packet)) + raise + remaining_packet = self.get_next_packet_info_for_interface2( + src_if.sw_if_index, + dst_if.sw_if_index, + packet_info) + self.assertIsNone(remaining_packet, + "Interface %s: Packet expected from interface " + "%s didn't arrive" % (dst_if.name, src_if.name)) + +5. Add the test code to test_basic function:: + + def test_basic(self): + count = 10 + # create the packet stream + packets = self.create_stream(self.pg0, self.pg1, count) + # add the stream to the source interface + self.pg0.add_stream(packets) + # enable capture on both interfaces + self.pg0.enable_capture() + self.pg1.enable_capture() + # start the packet generator + self.pg_start() + # get capture - the proper count of packets was saved by + # create_packet_info() based on dst_if parameter + capture = self.pg1.get_capture() + # assert nothing captured on pg0 (always do this last, so that + # some time has already passed since pg_start()) + self.pg0.assert_nothing_captured() + # verify capture + self.verify_capture(self.pg0, self.pg1, capture) + +6. Run the test by issuing 'make test'. -1. Add a new file called... -2. Add a setUpClass function containing... -3. Add the test code in test... -4. Run the test... |vtf| module documentation ########################## - + .. toctree:: :maxdepth: 2 :glob: diff --git a/test/framework.py b/test/framework.py index 896a1e0d..b2c6b9e4 100644 --- a/test/framework.py +++ b/test/framework.py @@ -117,7 +117,8 @@ class VppTestCase(unittest.TestCase): debug_cli = "" if cls.step or cls.debug_gdb or cls.debug_gdbserver: debug_cli = "cli-listen localhost:5002" - cls.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", debug_cli, "}", + cls.vpp_cmdline = [cls.vpp_bin, + "unix", "{", "nodaemon", debug_cli, "}", "api-segment", "{", "prefix", cls.shm_prefix, "}"] if cls.plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path]) @@ -246,8 +247,8 @@ class VppTestCase(unittest.TestCase): print(double_line_delim) print("VPP or GDB server is still running") print(single_line_delim) - raw_input("When done debugging, press ENTER to kill the process" - " and finish running the testcase...") + raw_input("When done debugging, press ENTER to kill the " + "process and finish running the testcase...") if hasattr(cls, 'vpp'): if hasattr(cls, 'vapi'): @@ -563,10 +564,10 @@ class VppTestResult(unittest.TestResult): 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 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) @@ -664,12 +665,12 @@ class VppTestResult(unittest.TestResult): unittest.TestResult.stopTest(self, test) if self.verbosity > 0: self.stream.writeln(single_line_delim) - self.stream.writeln("%-60s%s" % - (self.getDescription(test), self.result_string)) + 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)) def printErrors(self): """ diff --git a/test/hook.py b/test/hook.py index f3e5f880..247704ec 100644 --- a/test/hook.py +++ b/test/hook.py @@ -105,8 +105,8 @@ class PollHook(Hook): s = signaldict[abs(self.testcase.vpp.returncode)] else: s = "unknown" - msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( - self.testcase.vpp.returncode, s) + msg = "VPP subprocess died unexpectedly with returncode %d [%s]" %\ + (self.testcase.vpp.returncode, s) self.logger.critical(msg) core_path = self.testcase.tempdir + '/core' if os.path.isfile(core_path): diff --git a/test/template_bd.py b/test/template_bd.py index 91d5dd71..ae171351 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -75,8 +75,8 @@ class BridgeDomain(object): self.pg_start() - # Pick first received frame and check if it's the - # non-encapsulated frame + # Pick first received frame and check if it's the non-encapsulated + # frame out = self.pg1.get_capture(1) pkt = out[0] self.assert_eq_pkts(pkt, self.frame_request) diff --git a/test/test_classifier.py b/test/test_classifier.py index 302430f8..faa107dc 100644 --- a/test/test_classifier.py +++ b/test/test_classifier.py @@ -114,8 +114,9 @@ class TestClassifier(VppTestCase): payload_info = self.payload_to_info(str(packet[Raw])) packet_index = payload_info.index self.assertEqual(payload_info.dst, dst_sw_if_index) - self.logger.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]) @@ -296,8 +297,9 @@ 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_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)) @@ -326,7 +328,9 @@ class TestClassifier(VppTestCase): pkts = self.create_stream(self.pg0, self.pg3, self.pg_if_packet_sizes) self.pg0.add_stream(pkts) - self.create_classify_table('pbr', self.build_ip_mask(src_ip='ffffffff')) + 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'), diff --git a/test/test_flowperpkt.py b/test/test_flowperpkt.py index 29b3353b..f16bfb7e 100644 --- a/test/test_flowperpkt.py +++ b/test/test_flowperpkt.py @@ -57,8 +57,8 @@ class TestFlowperpkt(VppTestCase): if len(payload) * 2 != len(masked_expected_data): return False - # iterate over pairs: raw byte from payload and ASCII code for that byte - # from masked payload (or XX if masked) + # iterate over pairs: raw byte from payload and ASCII code for that + # byte from masked payload (or XX if masked) for i in range(len(payload)): p = payload[i] m = masked_expected_data[2 * i:2 * i + 2] diff --git a/test/test_ip4.py b/test/test_ip4.py index df93533d..9bd9a458 100644 --- a/test/test_ip4.py +++ b/test/test_ip4.py @@ -148,8 +148,9 @@ class TestIPv4(VppTestCase): payload_info = self.payload_to_info(str(packet[Raw])) packet_index = payload_info.index self.assertEqual(payload_info.dst, dst_sw_if_index) - self.logger.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]) @@ -209,7 +210,7 @@ class TestIPv4FibCrud(VppTestCase): - add new 1k, - del 1.5k - ..note:: Python API is to slow to add many routes, needs C code replacement. + ..note:: Python API is too slow to add many routes, needs replacement. """ def config_fib_many_to_one(self, start_dest_addr, next_hop_addr, count): @@ -221,8 +222,9 @@ class TestIPv4FibCrud(VppTestCase): :return list: added ips with 32 prefix """ added_ips = [] - dest_addr = int( - socket.inet_pton(socket.AF_INET, start_dest_addr).encode('hex'), 16) + dest_addr = int(socket.inet_pton(socket.AF_INET, + start_dest_addr).encode('hex'), + 16) dest_addr_len = 32 n_next_hop_addr = socket.inet_pton(socket.AF_INET, next_hop_addr) for _ in range(count): @@ -236,8 +238,9 @@ class TestIPv4FibCrud(VppTestCase): def unconfig_fib_many_to_one(self, start_dest_addr, next_hop_addr, count): removed_ips = [] - dest_addr = int( - socket.inet_pton(socket.AF_INET, start_dest_addr).encode('hex'), 16) + dest_addr = int(socket.inet_pton(socket.AF_INET, + start_dest_addr).encode('hex'), + 16) dest_addr_len = 32 n_next_hop_addr = socket.inet_pton(socket.AF_INET, next_hop_addr) for _ in range(count): diff --git a/test/test_ip4_vrf_multi_instance.py b/test/test_ip4_vrf_multi_instance.py index 1449ef7d..b4279194 100644 --- a/test/test_ip4_vrf_multi_instance.py +++ b/test/test_ip4_vrf_multi_instance.py @@ -95,7 +95,8 @@ class TestIp4VrfMultiInst(VppTestCase): try: # Create pg interfaces - cls.create_pg_interfaces(range(cls.nr_of_vrfs * cls.pg_ifs_per_vrf)) + cls.create_pg_interfaces( + range(cls.nr_of_vrfs * cls.pg_ifs_per_vrf)) # Packet flows mapping pg0 -> pg1, pg2 etc. cls.flows = dict() @@ -308,14 +309,14 @@ class TestIp4VrfMultiInst(VppTestCase): """ Create packet streams for all configured l2-pg interfaces, send all prepared packet streams and verify that: - - all packets received correctly on all pg-l2 interfaces assigned to - bridge domains + - all packets received correctly on all pg-l2 interfaces assigned + to bridge domains - no packet received on all pg-l2 interfaces not assigned to bridge domains :raise RuntimeError: If no packet captured on l2-pg interface assigned - to the bridge domain or if any packet is captured on l2-pg interface - not assigned to the bridge domain. + to the bridge domain or if any packet is captured on l2-pg + interface not assigned to the bridge domain. """ # Test # Create incoming packet streams for packet-generator interfaces diff --git a/test/test_ip6.py b/test/test_ip6.py index cd9c4b95..ea669b70 100644 --- a/test/test_ip6.py +++ b/test/test_ip6.py @@ -8,8 +8,8 @@ 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, ICMPv6ND_NS, ICMPv6ND_RS, ICMPv6ND_RA, \ - ICMPv6NDOptSrcLLAddr, getmacbyip6, ICMPv6MRD_Solicitation +from scapy.layers.inet6 import IPv6, UDP, ICMPv6ND_NS, ICMPv6ND_RS, \ + ICMPv6ND_RA, ICMPv6NDOptSrcLLAddr, getmacbyip6, ICMPv6MRD_Solicitation from util import ppp from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ptop, in6_islladdr, \ in6_mactoifaceid @@ -172,8 +172,9 @@ class TestIPv6(VppTestCase): payload_info = self.payload_to_info(str(packet[Raw])) packet_index = payload_info.index self.assertEqual(payload_info.dst, dst_sw_if_index) - self.logger.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]) @@ -229,9 +230,9 @@ class TestIPv6(VppTestCase): intf.assert_nothing_captured(remark=remark) def test_ns(self): - """ IPv6 Neighbour Soliciatation Exceptions + """ IPv6 Neighbour Solicitation Exceptions - Test sceanrio: + Test scenario: - Send an NS Sourced from an address not covered by the link sub-net - Send an NS to an mcast address the router has not joined - Send NS for a target address the router does not onn. @@ -249,12 +250,13 @@ class TestIPv6(VppTestCase): ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) pkts = [p] - self.send_and_assert_no_replies(self.pg0, pkts, - "No response to NS source by address not on sub-net") + self.send_and_assert_no_replies( + self.pg0, pkts, + "No response to NS source by address not on sub-net") # - # An NS for sent to a solicited mcast group the router is not a member of - # FAILS + # An NS for sent to a solicited mcast group the router is + # not a member of FAILS # if 0: nsma = in6_getnsma(inet_pton(socket.AF_INET6, "fd::ffff")) @@ -266,8 +268,9 @@ class TestIPv6(VppTestCase): ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) pkts = [p] - self.send_and_assert_no_replies(self.pg0, pkts, - "No response to NS sent to unjoined mcast address") + self.send_and_assert_no_replies( + self.pg0, pkts, + "No response to NS sent to unjoined mcast address") # # An NS whose target address is one the router does not own @@ -307,15 +310,15 @@ class TestIPv6(VppTestCase): in6_ptop(mk_ll_addr(intf.local_mac))) def test_rs(self): - """ IPv6 Router Soliciatation Exceptions + """ IPv6 Router Solicitation Exceptions - Test sceanrio: + Test scenario: """ # - # Before we begin change the IPv6 RA responses to use the unicast address - # that way we will not confuse them with the periodic Ras which go to the Mcast - # address + # Before we begin change the IPv6 RA responses to use the unicast + # address - that way we will not confuse them with the periodic + # RAs which go to the mcast address # self.pg0.ip6_ra_config(send_unicast=1) @@ -336,8 +339,8 @@ class TestIPv6(VppTestCase): # # When we reconfiure the IPv6 RA config, we reset the RA rate limiting, - # so we need to do this before each test below so as not to drop packets for - # rate limiting reasons. Test this works here. + # so we need to do this before each test below so as not to drop + # packets for rate limiting reasons. Test this works here. # self.pg0.ip6_ra_config(send_unicast=1) self.send_and_expect_ra(self.pg0, pkts, "Rate limit reset RS") @@ -366,12 +369,12 @@ class TestIPv6(VppTestCase): self.pg0, pkts, "RS sourced from link-local", src_ip=ll) # - # Source from the unspecified address ::. This happens when the RS is sent before - # the host has a configured address/sub-net, i.e. auto-config. - # Since the sender has no IP address, the reply comes back mcast - so the - # capture needs to not filter this. - # If we happen to pick up the periodic RA at this point then so be it, it's not - # an error. + # Source from the unspecified address ::. This happens when the RS + # is sent before the host has a configured address/sub-net, + # i.e. auto-config. Since the sender has no IP address, the reply + # comes back mcast - so the capture needs to not filter this. + # If we happen to pick up the periodic RA at this point then so be it, + # it's not an error. # self.pg0.ip6_ra_config(send_unicast=1) p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / @@ -399,9 +402,9 @@ class TestIPv6(VppTestCase): @unittest.skip("Unsupported") def test_mrs(self): - """ IPv6 Multicast Router Soliciatation Exceptions + """ IPv6 Multicast Router Solicitation Exceptions - Test sceanrio: + Test scenario: """ # diff --git a/test/test_l2_fib.py b/test/test_l2_fib.py index d4ef3f4a..3d5d2129 100644 --- a/test/test_l2_fib.py +++ b/test/test_l2_fib.py @@ -106,7 +106,8 @@ class TestL2fib(VppTestCase): # Create BD with MAC learning and unknown unicast flooding disabled # and put interfaces to this BD - cls.vapi.bridge_domain_add_del(bd_id=cls.bd_id, uu_flood=0, learn=0) + cls.vapi.bridge_domain_add_del( + bd_id=cls.bd_id, uu_flood=0, learn=0) for pg_if in cls.pg_interfaces: cls.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, bd_id=cls.bd_id) @@ -180,8 +181,8 @@ class TestL2fib(VppTestCase): end_nr = start + count / n_int for j in range(start, end_nr): host = self.hosts_by_pg_idx[pg_if.sw_if_index][j] - self.vapi.l2fib_add_del(host.mac, self.bd_id, pg_if.sw_if_index, - static_mac=1) + self.vapi.l2fib_add_del( + host.mac, self.bd_id, pg_if.sw_if_index, static_mac=1) counter += 1 percentage = counter / count * 100 if percentage > percent: @@ -202,8 +203,8 @@ class TestL2fib(VppTestCase): for pg_if in self.pg_interfaces: for j in range(count / n_int): host = self.hosts_by_pg_idx[pg_if.sw_if_index][0] - self.vapi.l2fib_add_del(host.mac, self.bd_id, pg_if.sw_if_index, - is_add=0) + self.vapi.l2fib_add_del( + host.mac, self.bd_id, pg_if.sw_if_index, is_add=0) self.deleted_hosts_by_pg_idx[pg_if.sw_if_index].append(host) del self.hosts_by_pg_idx[pg_if.sw_if_index][0] counter += 1 diff --git a/test/test_l2bd_multi_instance.py b/test/test_l2bd_multi_instance.py index a1226222..e24a8613 100644 --- a/test/test_l2bd_multi_instance.py +++ b/test/test_l2bd_multi_instance.py @@ -95,10 +95,10 @@ class TestL2bdMultiInst(VppTestCase): 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 + 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() @@ -172,8 +172,9 @@ class TestL2bdMultiInst(VppTestCase): def create_bd_and_mac_learn(self, count, start=1): """ - Create required number of bridge domains with MAC learning enabled, put - 3 l2-pg interfaces to every bridge domain and send MAC learning packets. + Create required number of bridge domains with MAC learning enabled, + put 3 l2-pg interfaces to every bridge domain and send MAC learning + packets. :param int count: Number of bridge domains to be created. :param int start: Starting number of the bridge domain ID. diff --git a/test/test_l2xc_multi_instance.py b/test/test_l2xc_multi_instance.py index 6c28cebb..bb26f959 100644 --- a/test/test_l2xc_multi_instance.py +++ b/test/test_l2xc_multi_instance.py @@ -2,10 +2,10 @@ """L2XC Multi-instance Test Case HLD: **NOTES:** - - higher number (more than 15) of pg-l2 interfaces causes problems => only \ - 14 pg-l2 interfaces and 10 cross-connects are tested - - jumbo packets in configuration with 14 l2-pg interfaces leads to \ - problems too + - higher number (more than 15) of pg-l2 interfaces causes problems => only + 14 pg-l2 interfaces and 10 cross-connects are tested + - jumbo packets in configuration with 14 l2-pg interfaces leads to + problems too **config 1** - add 14 pg-l2 interfaces @@ -15,7 +15,8 @@ - send L2 MAC frames between all pairs of pg-l2 interfaces **verify 1** - - all packets received correctly in case of cross-connected l2-pg interfaces + - all packets received correctly in case of cross-connected l2-pg + interfaces - no packet received in case of not cross-connected l2-pg interfaces **config 2** @@ -25,7 +26,8 @@ - send L2 MAC frames between all pairs of pg-l2 interfaces **verify 2** - - all packets received correctly in case of cross-connected l2-pg interfaces + - all packets received correctly in case of cross-connected l2-pg + interfaces - no packet received in case of not cross-connected l2-pg interfaces **config 3** @@ -35,7 +37,8 @@ - send L2 MAC frames between all pairs of pg-l2 interfaces **verify 3** - - all packets received correctly in case of cross-connected l2-pg interfaces + - all packets received correctly in case of cross-connected l2-pg + interfaces - no packet received in case of not cross-connected l2-pg interfaces **config 4** @@ -79,7 +82,8 @@ 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() diff --git a/test/test_snat.py b/test/test_snat.py index ca2e52a7..d23becf5 100644 --- a/test/test_snat.py +++ b/test/test_snat.py @@ -130,13 +130,15 @@ class TestSNAT(VppTestCase): if same_port: self.assertEqual(packet[TCP].sport, self.tcp_port_in) else: - self.assertNotEqual(packet[TCP].sport, self.tcp_port_in) + self.assertNotEqual( + packet[TCP].sport, self.tcp_port_in) self.tcp_port_out = packet[TCP].sport elif packet.haslayer(UDP): if same_port: self.assertEqual(packet[UDP].sport, self.udp_port_in) else: - self.assertNotEqual(packet[UDP].sport, self.udp_port_in) + self.assertNotEqual( + packet[UDP].sport, self.udp_port_in) self.udp_port_out = packet[UDP].sport else: if same_port: @@ -215,8 +217,14 @@ class TestSNAT(VppTestCase): addr_only = 0 l_ip = socket.inet_pton(socket.AF_INET, local_ip) e_ip = socket.inet_pton(socket.AF_INET, external_ip) - self.vapi.snat_add_static_mapping(l_ip, e_ip, local_port, external_port, - addr_only, vrf_id, is_add) + self.vapi.snat_add_static_mapping( + l_ip, + e_ip, + local_port, + external_port, + addr_only, + vrf_id, + is_add) def snat_add_address(self, ip, is_add=1): """ @@ -413,7 +421,9 @@ class TestSNAT(VppTestCase): 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) diff --git a/test/test_span.py b/test/test_span.py index 41507092..dc0110db 100644 --- a/test/test_span.py +++ b/test/test_span.py @@ -102,11 +102,12 @@ class TestSpan(VppTestCase): for i in self.interfaces: last_info[i.sw_if_index] = None dst_sw_if_index = dst_if.sw_if_index - if 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 + self.AssertEqual( + len(capture_pg1), + len(capture_pg2), + "Different number of outgoing and mirrored packets : %u != %u" % + (len(capture_pg1), + len(capture_pg2))) for pkt_pg1, pkt_pg2 in zip(capture_pg1, capture_pg2): try: ip1 = pkt_pg1[IP] diff --git a/test/vpp_interface.py b/test/vpp_interface.py index 9d9712eb..e0a29f94 100644 --- a/test/vpp_interface.py +++ b/test/vpp_interface.py @@ -15,7 +15,7 @@ class VppInterface(object): @property def remote_mac(self): - """MAC-address of the remote interface "connected" to this interface.""" + """MAC-address of the remote interface "connected" to this interface""" return self._remote_hosts[0].mac @property @@ -261,19 +261,23 @@ class VppInterface(object): def admin_up(self): """Put interface ADMIN-UP.""" - self.test.vapi.sw_interface_set_flags(self.sw_if_index, admin_up_down=1) + self.test.vapi.sw_interface_set_flags(self.sw_if_index, + admin_up_down=1) def admin_down(self): """Put interface ADMIN-down.""" - self.test.vapi.sw_interface_set_flags(self.sw_if_index, admin_up_down=0) + self.test.vapi.sw_interface_set_flags(self.sw_if_index, + admin_up_down=0) def ip6_enable(self): """IPv6 Enable interface""" - self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, enable=1) + self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, + enable=1) def ip6_disable(self): """Put interface ADMIN-DOWN.""" - self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, enable=0) + self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, + enable=0) def add_sub_if(self, sub_if): """Register a sub-interface with this interface. diff --git a/test/vpp_ip_route.py b/test/vpp_ip_route.py index 75f400f1..975e3934 100644 --- a/test/vpp_ip_route.py +++ b/test/vpp_ip_route.py @@ -13,7 +13,13 @@ MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 class RoutePath: - def __init__(self, nh_addr, nh_sw_if_index, nh_table_id=0, labels=[], nh_via_label=MPLS_LABEL_INVALID): + def __init__( + self, + nh_addr, + nh_sw_if_index, + nh_table_id=0, + labels=[], + nh_via_label=MPLS_LABEL_INVALID): self.nh_addr = socket.inet_pton(socket.AF_INET, nh_addr) self.nh_itf = nh_sw_if_index self.nh_table_id = nh_table_id @@ -36,15 +42,16 @@ class IpRoute: def add_vpp_config(self): for path in self.paths: - self._test.vapi.ip_add_del_route(self.dest_addr, - self.dest_addr_len, - path.nh_addr, - path.nh_itf, - table_id=self.table_id, - next_hop_out_label_stack=path.nh_labels, - next_hop_n_out_labels=len( - path.nh_labels), - next_hop_via_label=path.nh_via_label) + self._test.vapi.ip_add_del_route( + self.dest_addr, + self.dest_addr_len, + path.nh_addr, + path.nh_itf, + table_id=self.table_id, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len( + path.nh_labels), + next_hop_via_label=path.nh_via_label) def remove_vpp_config(self): for path in self.paths: @@ -61,7 +68,7 @@ class MplsIpBind: MPLS to IP Binding """ - def __init__(self, test, local_label, dest_addr, dest_addr_len): + def __init__(self, test, local_label, dest_addr, dest_addr_len): self._test = test self.dest_addr = socket.inet_pton(socket.AF_INET, dest_addr) self.dest_addr_len = dest_addr_len @@ -93,17 +100,18 @@ class MplsRoute: def add_vpp_config(self): for path in self.paths: - self._test.vapi.mpls_route_add_del(self.local_label, - self.eos_bit, - 1, - path.nh_addr, - path.nh_itf, - table_id=self.table_id, - next_hop_out_label_stack=path.nh_labels, - next_hop_n_out_labels=len( - path.nh_labels), - next_hop_via_label=path.nh_via_label, - next_hop_table_id=path.nh_table_id) + self._test.vapi.mpls_route_add_del( + self.local_label, + self.eos_bit, + 1, + path.nh_addr, + path.nh_itf, + table_id=self.table_id, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len( + path.nh_labels), + next_hop_via_label=path.nh_via_label, + next_hop_table_id=path.nh_table_id) def remove_vpp_config(self): for path in self.paths: diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 26edd4f2..b78e8613 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -5,9 +5,9 @@ from hook import Hook from collections import deque # 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. +# 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") @@ -224,8 +224,8 @@ class VppPapiProvider(object): send_unicast,): return self.api(self.papi.sw_interface_ip6nd_ra_config, {'sw_if_index': sw_if_index, - 'suppress' : suppress, - 'send_unicast' : send_unicast}) + 'suppress': suppress, + 'send_unicast': send_unicast}) def ip6_sw_interface_enable_disable(self, sw_if_index, enable): """ @@ -972,7 +972,7 @@ class VppPapiProvider(object): """ :param is_add: :param mask: - :param match_n_vectors: (Default value = 1): + :param match_n_vectors: (Default value = 1) :param table_index: (Default value = 0xFFFFFFFF) :param nbuckets: (Default value = 2) :param memory_size: (Default value = 2097152) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index eeb9c1a5..756f79b5 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -13,6 +13,7 @@ from util import ppp, ppc from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ismaddr from scapy.utils import inet_pton, inet_ntop + def is_ipv6_misc(p): """ Is packet one of uninteresting IPv6 broadcasts? """ if p.haslayer(ICMPv6ND_RA): @@ -91,10 +92,12 @@ class VppPGInterface(VppInterface): 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) + self._input_cli = \ + "packet-generator new pcap %s source pg%u name %s" % ( + self.in_path, self.pg_index, self.cap_name) - def rotate_out_file(self): + def enable_capture(self): + """ Enable capture on this packet-generator interface""" try: if os.path.isfile(self.out_path): os.rename(self.out_path, @@ -106,10 +109,6 @@ class VppPGInterface(VppInterface): self._out_file)) except: pass - - def enable_capture(self): - """ Enable capture on this packet-generator interface""" - self.rotate_out_file() # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.capture_cli) self._pcap_reader = None @@ -151,7 +150,7 @@ class VppPGInterface(VppInterface): before = len(output.res) if filter_out_fn: output.res = [p for p in output.res if not filter_out_fn(p)] - removed = before - len(output.res) + removed = len(output.res) - before if removed: self.test.logger.debug( "Filtered out %s packets from capture (returning %s)" % @@ -181,9 +180,13 @@ class VppPGInterface(VppInterface): based_on = "based on stored packet_infos" if expected_count == 0: raise Exception( - "Internal error, expected packet count for %s is 0!" % name) + "Internal error, expected packet count for %s is 0!" % + name) 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) @@ -192,8 +195,6 @@ class VppPGInterface(VppInterface): if len(capture.res) == expected_count: # bingo, got the packets we expected return capture - elif expected_count == 0: - return None remaining_time -= elapsed_time if capture: raise Exception("Captured packets mismatch, captured %s packets, " @@ -241,8 +242,9 @@ class VppPGInterface(VppInterface): 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: if os.path.isfile(self.out_path): @@ -275,8 +277,10 @@ class VppPGInterface(VppInterface): self._pcap_reader = PcapReader(self.out_path) break except: - self.test.logger.debug("Exception in scapy.PcapReader(%s): " - "%s" % (self.out_path, format_exc())) + 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) @@ -290,8 +294,9 @@ class VppPGInterface(VppInterface): "Packet received after %ss was filtered out" % (time.time() - (deadline - timeout))) else: - self.test.logger.debug("Packet received after %fs" % - (time.time() - (deadline - timeout))) + 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") @@ -335,7 +340,6 @@ class VppPGInterface(VppInterface): self.test.logger.info("No ARP received on port %s" % pg_interface.name) return - self.rotate_out_file() arp_reply = captured_packet.copy() # keep original for exception # Make Dot1AD packet content recognizable to scapy if arp_reply.type == 0x88a8: @@ -380,7 +384,8 @@ class VppPGInterface(VppInterface): captured_packet = pg_interface.wait_for_packet( deadline - now, filter_out_fn=None) except: - self.test.logger.error("Timeout while waiting for NDP response") + 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 @@ -395,13 +400,12 @@ class VppPGInterface(VppInterface): self._local_mac = opt.lladdr self.test.logger.debug(self.test.vapi.cli("show trace")) # we now have the MAC we've been after - self.rotate_out_file() return except: self.test.logger.info( - ppp("Unexpected response to NDP request:", captured_packet)) + ppp("Unexpected response to NDP request:", + captured_packet)) now = time.time() self.test.logger.debug(self.test.vapi.cli("show trace")) - self.rotate_out_file() raise Exception("Timeout while waiting for NDP response") -- 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/Makefile') 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 7ff11136b1a528bd3d9b0795fa0005b40c2b3b9f Mon Sep 17 00:00:00 2001 From: Tibor Date: Thu, 26 Jan 2017 13:21:12 +0100 Subject: Add sphinx read the doc theme. Change-Id: Iade7c42081378bf211b81ad29b2898518442d2ff Signed-off-by: Tibor --- test/Makefile | 2 +- test/doc/conf.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index db64ad93..f3c71b51 100644 --- a/test/Makefile +++ b/test/Makefile @@ -55,7 +55,7 @@ wipe: reset doc: verify-python-path @virtualenv $(PYTHON_VENV_PATH) -p python2.7 - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx sphinx-rtd-theme" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" .PHONY: wipe-doc diff --git a/test/doc/conf.py b/test/doc/conf.py index 96ea50fe..ec8958ea 100644 --- a/test/doc/conf.py +++ b/test/doc/conf.py @@ -122,7 +122,8 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the -- cgit 1.2.3-korg From 6f692d6e5a8ffc920a728372ef773199bc5466c0 Mon Sep 17 00:00:00 2001 From: Dave Wallace Date: Thu, 26 Jan 2017 21:07:10 -0500 Subject: Fix make test scapy python patch issue, VPP-615 Change-Id: I1e6798905f0049c793224a2c880f3b2cfadcf1e7 Signed-off-by: Dave Wallace --- test/Makefile | 1 + 1 file changed, 1 insertion(+) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index f3c71b51..ef194952 100644 --- a/test/Makefile +++ b/test/Makefile @@ -23,6 +23,7 @@ $(PIP_INSTALL_DONE): $(PIP_PATCH_DONE): $(PIP_INSTALL_DONE) @echo --- patching --- + @sleep 1 # Ensure python recompiles patched *.py files -> *.pyc for f in $(CURDIR)/patches/scapy-2.3.3/*.patch ; do \ echo Applying patch: $$(basename $$f) ; \ patch -p1 -d $(SCAPY_SOURCE) < $$f ; \ -- cgit 1.2.3-korg From aaa396ac6bb27549daa2f9ef644325685917ed16 Mon Sep 17 00:00:00 2001 From: Neale Ranns Date: Sun, 5 Feb 2017 09:12:02 -0800 Subject: Test checksyle for IP mcast tests Change-Id: I20b61b7e63f74b4656d84717633e06646514c5eb Signed-off-by: Neale Ranns --- test/Makefile | 2 +- test/test_ip_mcast.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index ef194952..45a0218a 100644 --- a/test/Makefile +++ b/test/Makefile @@ -81,7 +81,7 @@ wipe-cov: wipe .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 && pip install pep8" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate &&\ pep8 --show-source -v $(WS_ROOT)/test/*.py ||\ (echo \"*******************************************************************\" &&\ diff --git a/test/test_ip_mcast.py b/test/test_ip_mcast.py index 9b668e42..2e0db430 100644 --- a/test/test_ip_mcast.py +++ b/test/test_ip_mcast.py @@ -32,8 +32,8 @@ class MRouteEntryFlags: # # The number of packets sent is set to 90 so that when we replicate more than 3 # times, which we do for some entries, we will generate more than 256 packets -# to the next node in the VLIB graph. Thus we are testing the code's correctness -# handling this over-flow +# to the next node in the VLIB graph. Thus we are testing the code's +# correctness handling this over-flow # N_PKTS_IN_STREAM = 90 -- cgit 1.2.3-korg From 63205141704cb1adafd1b5108f787e640eda71e9 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Fri, 3 Feb 2017 08:18:53 +0100 Subject: make test: fix dependencies checkstyle - doesn't need scapy/pexpect, remove it doc - scapy wasn't patched properly, fix it Change-Id: I65202cb14edeb239d21ce10f17d9b4fccce43d62 Signed-off-by: Klement Sekera --- test/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 45a0218a..7a18be1d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -54,9 +54,9 @@ wipe: reset @rm -rf $(PYTHON_VENV_PATH) @rm -f $(PAPI_INSTALL_FLAGS) -doc: verify-python-path +doc: verify-python-path $(PIP_PATCH_DONE) @virtualenv $(PYTHON_VENV_PATH) -p python2.7 - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS) sphinx sphinx-rtd-theme" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx sphinx-rtd-theme" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" .PHONY: wipe-doc -- cgit 1.2.3-korg From 104543fa6a836df84b5b6d9b1909df3d5226a1e7 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Fri, 3 Feb 2017 07:29:43 +0100 Subject: make test: improve test filtering Implement fine-grained test filtering by supporting more complicated filters beside the original file name suffix filter. Change-Id: If5a166d08cffe8c58cc6cf174e6df861c34dbaa6 Signed-off-by: Klement Sekera --- Makefile | 2 +- test/Makefile | 12 ++++++++-- test/framework.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 5 deletions(-) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index b50d2039..bbbb2acf 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ help: @echo " startup.conf file is present" @echo " GDB= - gdb binary to use for debugging" @echo " PLATFORM= - target platform. default is vpp" - @echo " TEST= - only run specific test" + @echo " TEST= - apply filter to test set, see test-help" @echo "" @echo "Current Argument Values:" @echo " V = $(V)" diff --git a/test/Makefile b/test/Makefile index 7a18be1d..5c0d48f0 100644 --- a/test/Makefile +++ b/test/Makefile @@ -35,7 +35,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_$(TEST)\"*.py\"" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_\"*.py\"" endef test: reset verify-python-path $(PAPI_INSTALL_DONE) @@ -111,7 +111,15 @@ help: @echo " DEBUG=gdbserver - run gdb inside a gdb server, otherwise " @echo " same as above" @echo " STEP=[yes|no] - ease debugging by stepping through a testcase " - @echo " TEST= - only run specific test" + @echo " TEST= - filter the set of tests:" + @echo " by file-name - only run tests from specified file, e.g. TEST=test_bfd selects all tests from test_bfd.py" + @echo " by file-suffix - same as file-name, but 'test_' is omitted e.g. TEST=bfd selects all tests from test_bfd.py" + @echo " by wildcard - wildcard filter is .., each can be replaced by '*'" + @echo " e.g. TEST='test_bfd.*.*' is equivalent to above example of filter by file-name" + @echo " TEST='bfd.*.*' is equivalent to above example of filter by file-suffix" + @echo " TEST='bfd.BFDAPITestCase.*' selects all tests from test_bfd.py which are part of BFDAPITestCase class" + @echo " TEST='bfd.BFDAPITestCase.test_add_bfd' selects a single test named test_add_bfd from test_bfd.py/BFDAPITestCase" + @echo " TEST='*.*.test_add_bfd' selects all test functions named test_add_bfd from all files/classes" @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" diff --git a/test/framework.py b/test/framework.py index 8ceb33c3..889a3046 100644 --- a/test/framework.py +++ b/test/framework.py @@ -698,7 +698,7 @@ class VppTestResult(unittest.TestResult): class VppTestRunner(unittest.TextTestRunner): """ - A basic test runner implementation which prints results on standard error. + A basic test runner implementation which prints results to standard error. """ @property def resultclass(self): @@ -713,6 +713,67 @@ class VppTestRunner(unittest.TextTestRunner): verbosity, failfast, buffer, resultclass) + test_option = "TEST" + + def parse_test_option(self): + try: + f = os.getenv(self.test_option) + except: + f = None + filter_file_name = None + filter_class_name = None + filter_func_name = None + if f: + if '.' in f: + parts = f.split('.') + if len(parts) > 3: + raise Exception("Unrecognized %s option: %s" % + (self.test_option, f)) + if len(parts) > 2: + if parts[2] not in ('*', ''): + filter_func_name = parts[2] + if parts[1] not in ('*', ''): + filter_class_name = parts[1] + if parts[0] not in ('*', ''): + if parts[0].startswith('test_'): + filter_file_name = parts[0] + else: + filter_file_name = 'test_%s' % parts[0] + else: + if f.startswith('test_'): + filter_file_name = f + else: + filter_file_name = 'test_%s' % f + return filter_file_name, filter_class_name, filter_func_name + + def filter_tests(self, tests, filter_file, filter_class, filter_func): + result = unittest.suite.TestSuite() + for t in tests: + if isinstance(t, unittest.suite.TestSuite): + # this is a bunch of tests, recursively filter... + x = self.filter_tests(t, filter_file, filter_class, + filter_func) + if x.countTestCases() > 0: + result.addTest(x) + elif isinstance(t, unittest.TestCase): + # this is a single test + parts = t.id().split('.') + # t.id() for common cases like this: + # test_classifier.TestClassifier.test_acl_ip + # apply filtering only if it is so + if len(parts) == 3: + if filter_file and filter_file != parts[0]: + continue + if filter_class and filter_class != parts[1]: + continue + if filter_func and filter_func != parts[2]: + continue + result.addTest(t) + else: + # unexpected object, don't touch it + result.addTest(t) + return result + def run(self, test): """ Run the tests @@ -721,4 +782,11 @@ class VppTestRunner(unittest.TextTestRunner): """ print("Running tests using custom test runner") # debug message - return super(VppTestRunner, self).run(test) + filter_file, filter_class, filter_func = self.parse_test_option() + print("Active filters: file=%s, class=%s, function=%s" % ( + filter_file, filter_class, filter_func)) + filtered = self.filter_tests(test, filter_file, filter_class, + filter_func) + print("%s out of %s tests match specified filters" % ( + filtered.countTestCases(), test.countTestCases())) + return super(VppTestRunner, self).run(filtered) -- 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/Makefile') 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/Makefile') 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 b93d1424301978405b53784d1e85a952dd78f4e5 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 2 Mar 2017 08:16:20 +0100 Subject: make test: don't run if other vpp process runs Check if there are existing vpp processes before running the test suite and refuse to run if there are. This prevents the removal of other test suite temporary files and also makes sure that if the machine is loaded by (zombie) vpp processes, interactive tests (like bfd) won't fail. Change-Id: I88a74098188cb3f51966de5db19d7f80f39e51e2 Signed-off-by: Klement Sekera --- test/Makefile | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index fd1bc0a4..25c8e0a8 100644 --- a/test/Makefile +++ b/test/Makefile @@ -5,6 +5,24 @@ ifndef VPP_PYTHON_PREFIX $(error VPP_PYTHON_PREFIX is not set) endif +.PHONY: verify-no-running-vpp + +ifdef VPP_ZOMBIE_NOCHECK +VPP_PIDS= +else +VPP_PIDS=$(shell pgrep -d, -x vpp_main) +endif + +verify-no-running-vpp: + @if [ "$(VPP_PIDS)" != "" ]; then \ + echo; \ + echo "*** Existing vpp processes detected (PID(s): $(VPP_PIDS)). Running tests under these conditions is not supported. ***"; \ + echo; \ + ps -fp $(VPP_PIDS);\ + echo; \ + false; \ + fi + UNITTEST_EXTRA_OPTS="" ifeq ($(FAILFAST),1) @@ -44,10 +62,10 @@ define retest-func @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) +test: verify-python-path verify-no-running-vpp reset $(PAPI_INSTALL_DONE) $(call retest-func) -retest: reset verify-python-path +retest: verify-python-path verify-no-running-vpp reset $(call retest-func) .PHONY: wipe doc @@ -128,6 +146,8 @@ help: @echo " TEST='bfd.BFDAPITestCase.test_add_bfd' selects a single test named test_add_bfd from test_bfd.py/BFDAPITestCase" @echo " TEST='*.*.test_add_bfd' selects all test functions named test_add_bfd from all files/classes" @echo "" + @echo " VPP_ZOMBIE_NOCHECK=1 - skip checking for vpp (zombie) processes (CAUTION)" + @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" @echo " test-wipe-doc - wipe documentation for test framework" -- cgit 1.2.3-korg From 80a7f0a81cf6bc58cd7567c7b5e1c75813b5fc0c Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 2 Mar 2017 11:27:11 +0100 Subject: make test: tell vpp to set coredump size Unless overridden by COREDUMP_SIZE env variable, tell VPP to set coredump size to unlimited, otherwise use $COREDUMP_SIZE as the argument. Change-Id: Ia2a6508207c66a171b33d272c820b1deb4a83e82 Signed-off-by: Klement Sekera --- test/Makefile | 3 +++ test/framework.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 25c8e0a8..52980b9e 100644 --- a/test/Makefile +++ b/test/Makefile @@ -147,6 +147,9 @@ help: @echo " TEST='*.*.test_add_bfd' selects all test functions named test_add_bfd from all files/classes" @echo "" @echo " VPP_ZOMBIE_NOCHECK=1 - skip checking for vpp (zombie) processes (CAUTION)" + @echo " COREDUMP_SIZE= - pass as unix { coredump-size } argument to vpp" + @echo " e.g. COREDUMP_SIZE=4g" + @echo " COREDUMP_SIZE=unlimited" @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" diff --git a/test/framework.py b/test/framework.py index 3bbd37d5..b9a09de2 100644 --- a/test/framework.py +++ b/test/framework.py @@ -110,10 +110,6 @@ class VppTestCase(unittest.TestCase): return dl = d.lower() if dl == "core": - if resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0: - # give a heads up if this is actually useless - print(colorize("WARNING: core size limit is set 0, core files " - "will NOT be created", RED)) cls.debug_core = True elif dl == "gdb": cls.debug_gdb = True @@ -140,8 +136,17 @@ class VppTestCase(unittest.TestCase): debug_cli = "" if cls.step or cls.debug_gdb or cls.debug_gdbserver: debug_cli = "cli-listen localhost:5002" - cls.vpp_cmdline = [cls.vpp_bin, - "unix", "{", "nodaemon", debug_cli, "}", + coredump_size = None + try: + size = os.getenv("COREDUMP_SIZE") + if size is not None: + coredump_size = "coredump-size %s" % size + except: + pass + if coredump_size is None: + coredump_size = "coredump-size unlimited" + cls.vpp_cmdline = [cls.vpp_bin, "unix", + "{", "nodaemon", debug_cli, coredump_size, "}", "api-trace", "{", "on", "}", "api-segment", "{", "prefix", cls.shm_prefix, "}"] if cls.plugin_path is not None: -- cgit 1.2.3-korg From 871349371a62f1f20b159b6afead8e84f8a2322b Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 7 Mar 2017 11:39:27 +0100 Subject: make test: split into basic and extended tests Implement plumbing to allow decorating tests as extended, e.g.: @unittest.skipUnless(running_extended_tests(), "part of extended tests") both methods and classes can be decorated this way. Change make test and make test-debug to run only non-extended tests. Introduce make test-all and make test-all-debug to run the full suite. Run full suite as part of make verify. Decorate most BFD tests as extended. Change-Id: I3bc64f59e9fe238f7f767d7e043dc165d03e9dfa Signed-off-by: Klement Sekera --- Makefile | 17 ++++++++++++++--- test/Makefile | 6 ++++-- test/framework.py | 30 +++++++++++++++++++++++++++--- test/test_bfd.py | 35 +++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 10 deletions(-) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 9161a384..7faba3b6 100644 --- a/Makefile +++ b/Makefile @@ -82,8 +82,10 @@ help: @echo " run-release - run release binary" @echo " debug - run debug binary with debugger" @echo " debug-release - run release binary with debugger" - @echo " test - build and run functional tests" - @echo " test-debug - build and run functional tests (debug build)" + @echo " test - build and run (basic) functional tests" + @echo " test-debug - build and run (basic) functional tests (debug build)" + @echo " test-all - build and run (all) functional tests" + @echo " test-all-debug - build and run (all) functional tests (debug build)" @echo " test-wipe - wipe files generated by unit tests" @echo " retest - run functional tests" @echo " retest-debug - run functional tests (debug build)" @@ -221,6 +223,7 @@ define test VPP_TEST_PLUGIN_PATH=$(BR)/install-$(2)-native/vpp/lib64/vpp_plugins \ VPP_TEST_INSTALL_PATH=$(BR)/install-$(2)-native/ \ LD_LIBRARY_PATH=$(BR)/install-$(2)-native/vpp/lib64/ \ + EXTENDED_TESTS=$(EXTENDED_TESTS) \ $(3) endef @@ -230,6 +233,14 @@ test: bootstrap test-debug: bootstrap $(call test,vpp_lite,vpp_lite_debug,test) +test-all: bootstrap + $(eval EXTENDED_TESTS=yes) + $(call test,vpp_lite,vpp_lite,test) + +test-all-debug: bootstrap + $(eval EXTENDED_TESTS=yes) + $(call test,vpp_lite,vpp_lite_debug,test) + test-help: @make -C test help @@ -368,6 +379,6 @@ ifeq ($(OS_ID)-$(OS_VERSION_ID),ubuntu-16.04) endif $(call banner,"Building $(PKG) packages") @make pkg-$(PKG) - @make test + @make test-all diff --git a/test/Makefile b/test/Makefile index 52980b9e..1bb3c6c2 100644 --- a/test/Makefile +++ b/test/Makefile @@ -119,8 +119,10 @@ checkstyle: verify-python-path help: @echo "Running tests:" @echo "" - @echo " test - build and run functional tests" - @echo " test-debug - build and run functional tests (debug build)" + @echo " test - build and run (basic) functional tests" + @echo " test-debug - build and run (basic) functional tests (debug build)" + @echo " test-all - build and run (all) functional tests" + @echo " test-all-debug - build and run (all) functional tests (debug build)" @echo " retest - run functional tests" @echo " retest-debug - run functional tests (debug build)" @echo " test-wipe - wipe (temporary) files generated by unit tests" diff --git a/test/framework.py b/test/framework.py index b7e6b4a0..6b1799a5 100644 --- a/test/framework.py +++ b/test/framework.py @@ -78,6 +78,15 @@ def pump_output(testclass): # of properly terminating the loop +def running_extended_tests(): + try: + s = os.getenv("EXTENDED_TESTS") + return True if s.lower() in ("y", "yes", "1") else False + except: + return False + return False + + class VppTestCase(unittest.TestCase): """This subclass is a base class for VPP test cases that are implemented as classes. It provides methods to create and run test case. @@ -230,9 +239,6 @@ class VppTestCase(unittest.TestCase): cls.verbose = 0 cls.vpp_dead = False cls.registry = VppObjectRegistry() - print(double_line_delim) - print(colorize(getdoc(cls).splitlines()[0], YELLOW)) - 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: @@ -613,6 +619,22 @@ class VppTestCase(unittest.TestCase): time.sleep(timeout) +class TestCasePrinter(object): + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + if not hasattr(self, "_test_case_set"): + self._test_case_set = set() + + def print_test_case_heading_if_first_time(self, case): + if case.__class__ not in self._test_case_set: + print(double_line_delim) + print(colorize(getdoc(case.__class__).splitlines()[0], YELLOW)) + print(double_line_delim) + self._test_case_set.add(case.__class__) + + class VppTestResult(unittest.TestResult): """ @property result_string @@ -641,6 +663,7 @@ class VppTestResult(unittest.TestResult): self.descriptions = descriptions self.verbosity = verbosity self.result_string = None + self.printer = TestCasePrinter() def addSuccess(self, test): """ @@ -740,6 +763,7 @@ class VppTestResult(unittest.TestResult): :param test: """ + self.printer.print_test_case_heading_if_first_time(test) unittest.TestResult.startTest(self, test) if self.verbosity > 0: self.stream.writeln( diff --git a/test/test_bfd.py b/test/test_bfd.py index e7ebb214..5460a2b5 100644 --- a/test/test_bfd.py +++ b/test/test_bfd.py @@ -15,7 +15,7 @@ from scapy.layers.inet import UDP, IP from scapy.layers.inet6 import IPv6 from bfd import VppBFDAuthKey, BFD, BFDAuthType, VppBFDUDPSession, \ BFDDiagCode, BFDState, BFD_vpp_echo -from framework import VppTestCase, VppTestRunner +from framework import VppTestCase, VppTestRunner, running_extended_tests from vpp_pg_interface import CaptureTimeoutError from util import ppp from vpp_papi_provider import UnexpectedApiReturnValueError @@ -40,6 +40,7 @@ class AuthKeyFactory(object): conf_key_id=conf_key_id, key=key) +@unittest.skipUnless(running_extended_tests(), "part of extended tests") class BFDAPITestCase(VppTestCase): """Bidirectional Forwarding Detection (BFD) - API""" @@ -692,6 +693,7 @@ class BFD4TestCase(VppTestCase): bfd_session_up(self) bfd_session_down(self) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_hold_up(self): """ hold BFD session up """ bfd_session_up(self) @@ -701,6 +703,7 @@ class BFD4TestCase(VppTestCase): self.assert_equal(len(self.vapi.collect_events()), 0, "number of bfd events") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_slow_timer(self): """ verify slow periodic control frames while session down """ packet_count = 3 @@ -715,6 +718,7 @@ class BFD4TestCase(VppTestCase): time_diff, 0.70, 1.05, "time between slow packets") prev_packet = next_packet + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_zero_remote_min_rx(self): """ no packets when zero remote required min rx interval """ bfd_session_up(self) @@ -739,6 +743,7 @@ class BFD4TestCase(VppTestCase): self.assert_equal( len(self.vapi.collect_events()), 0, "number of bfd events") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_conn_down(self): """ verify session goes down after inactivity """ bfd_session_up(self) @@ -748,6 +753,7 @@ class BFD4TestCase(VppTestCase): e = self.vapi.wait_for_event(1, "bfd_udp_session_details") verify_event(self, e, expected_state=BFDState.down) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_large_required_min_rx(self): """ large remote required min rx interval """ bfd_session_up(self) @@ -777,6 +783,7 @@ class BFD4TestCase(VppTestCase): break self.assert_equal(count, 0, "number of packets received") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_immediate_remote_min_rx_reduction(self): """ immediately honor remote required min rx reduction """ self.vpp_session.remove_vpp_config() @@ -809,6 +816,7 @@ class BFD4TestCase(VppTestCase): "time between BFD packets") reference_packet = p + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_modify_req_min_rx_double(self): """ modify session - double required min rx """ bfd_session_up(self) @@ -838,6 +846,7 @@ class BFD4TestCase(VppTestCase): self.assert_in_range(time_to_event, .9 * timeout, 1.1 * timeout, "session timeout") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_modify_req_min_rx_halve(self): """ modify session - halve required min rx """ self.vpp_session.modify_parameters( @@ -878,6 +887,7 @@ class BFD4TestCase(VppTestCase): "time before bfd session goes down") verify_event(self, e, expected_state=BFDState.down) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_modify_detect_mult(self): """ modify detect multiplier """ bfd_session_up(self) @@ -901,6 +911,7 @@ class BFD4TestCase(VppTestCase): self.assertNotIn("P", p.sprintf("%BFD.flags%"), "Poll bit not set in BFD packet") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_queued_poll(self): """ test poll sequence queueing """ bfd_session_up(self) @@ -966,6 +977,7 @@ class BFD4TestCase(VppTestCase): self.assertNotIn("P", p.sprintf("%BFD.flags%"), "Poll bit set in BFD packet") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_poll_response(self): """ test correct response to control frame with poll bit set """ bfd_session_up(self) @@ -976,6 +988,7 @@ class BFD4TestCase(VppTestCase): self, pcap_time_min=time.time() - self.vpp_clock_offset) self.assertIn("F", final.sprintf("%BFD.flags%")) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_no_periodic_if_remote_demand(self): """ no periodic frames outside poll sequence if remote demand set """ bfd_session_up(self) @@ -1093,6 +1106,7 @@ class BFD4TestCase(VppTestCase): "number of bfd events") self.test_session.send_packet() + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_echo_fail(self): """ session goes down if echo function fails """ bfd_session_up(self) @@ -1132,6 +1146,7 @@ class BFD4TestCase(VppTestCase): self.assert_equal(events[0].state, BFDState.down, BFDState) self.assertTrue(verified_diag, "Incorrect diagnostics code received") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_echo_stop(self): """ echo function stops if peer sets required min echo rx zero """ bfd_session_up(self) @@ -1162,6 +1177,7 @@ class BFD4TestCase(VppTestCase): events = self.vapi.collect_events() self.assert_equal(len(events), 0, "number of bfd events") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_echo_source_removed(self): """ echo function stops if echo source is removed """ bfd_session_up(self) @@ -1192,6 +1208,7 @@ class BFD4TestCase(VppTestCase): events = self.vapi.collect_events() self.assert_equal(len(events), 0, "number of bfd events") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_stale_echo(self): """ stale echo packets don't keep a session up """ bfd_session_up(self) @@ -1243,6 +1260,7 @@ class BFD4TestCase(VppTestCase): self.test_session.send_packet() self.assertTrue(timeout_ok, "Expected timeout event didn't occur") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_invalid_echo_checksum(self): """ echo packets with invalid checksum don't keep a session up """ bfd_session_up(self) @@ -1291,6 +1309,7 @@ class BFD4TestCase(VppTestCase): self.test_session.send_packet() self.assertTrue(timeout_ok, "Expected timeout event didn't occur") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_admin_up_down(self): """ put session admin-up and admin-down """ bfd_session_up(self) @@ -1328,6 +1347,7 @@ class BFD4TestCase(VppTestCase): e = self.vapi.wait_for_event(1, "bfd_udp_session_details") verify_event(self, e, expected_state=BFDState.up) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_config_change_remote_demand(self): """ configuration change while peer in demand mode """ bfd_session_up(self) @@ -1445,6 +1465,7 @@ class BFD6TestCase(VppTestCase): self.test_session.send_packet() self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_hold_up(self): """ hold BFD session up """ bfd_session_up(self) @@ -1502,6 +1523,7 @@ class BFD6TestCase(VppTestCase): self.assert_equal(udp_sport_tx, udp_sport_rx, "UDP source port (== " "ECHO packet identifier for test purposes)") + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_echo(self): """ echo function used """ bfd_session_up(self) @@ -1596,6 +1618,7 @@ class BFDSHA1TestCase(VppTestCase): bfd_key_id=self.vpp_session.bfd_key_id) bfd_session_up(self) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_hold_up(self): """ hold BFD session up """ key = self.factory.create_random_key(self) @@ -1614,6 +1637,7 @@ class BFDSHA1TestCase(VppTestCase): self.test_session.send_packet() self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_hold_up_meticulous(self): """ hold BFD session up - meticulous auth """ key = self.factory.create_random_key( @@ -1635,6 +1659,7 @@ class BFDSHA1TestCase(VppTestCase): self.test_session.send_packet() self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_send_bad_seq_number(self): """ session is not kept alive by msgs with bad sequence numbers""" key = self.factory.create_random_key( @@ -1697,6 +1722,7 @@ class BFDSHA1TestCase(VppTestCase): wait_for_bfd_packet(self) self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_mismatch_auth(self): """ session is not brought down by unauthenticated msg """ key = self.factory.create_random_key(self) @@ -1711,6 +1737,7 @@ class BFDSHA1TestCase(VppTestCase): legitimate_test_session, rogue_test_session) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_mismatch_bfd_key_id(self): """ session is not brought down by msg with non-existent key-id """ key = self.factory.create_random_key(self) @@ -1730,6 +1757,7 @@ class BFDSHA1TestCase(VppTestCase): legitimate_test_session, rogue_test_session) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_mismatched_auth_type(self): """ session is not brought down by msg with wrong auth type """ key = self.factory.create_random_key(self) @@ -1746,6 +1774,7 @@ class BFDSHA1TestCase(VppTestCase): vpp_session, legitimate_test_session, rogue_test_session, {'auth_type': BFDAuthType.keyed_md5}) + @unittest.skipUnless(running_extended_tests(), "part of extended tests") def test_restart(self): """ simulate remote peer restart and resynchronization """ key = self.factory.create_random_key( @@ -1761,7 +1790,7 @@ class BFDSHA1TestCase(VppTestCase): # don't send any packets for 2*detection_time detection_time = self.test_session.detect_mult *\ self.vpp_session.required_min_rx / USEC_IN_SEC - self.sleep(2*detection_time, "simulating peer restart") + self.sleep(2 * detection_time, "simulating peer restart") events = self.vapi.collect_events() self.assert_equal(len(events), 1, "number of bfd events") verify_event(self, events[0], expected_state=BFDState.down) @@ -1774,6 +1803,7 @@ class BFDSHA1TestCase(VppTestCase): bfd_session_up(self) +@unittest.skipUnless(running_extended_tests(), "part of extended tests") class BFDAuthOnOffTestCase(VppTestCase): """Bidirectional Forwarding Detection (BFD) (changing auth) """ @@ -1981,6 +2011,7 @@ class BFDAuthOnOffTestCase(VppTestCase): "number of bfd events") +@unittest.skipUnless(running_extended_tests(), "part of extended tests") class BFDCLITestCase(VppTestCase): """Bidirectional Forwarding Detection (BFD) (CLI) """ pg0 = None -- cgit 1.2.3-korg From a86e289570dfb7e02c47bed023c0669f3804a8bd Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 9 Mar 2017 08:01:52 +0100 Subject: make test: add make test-shell[-debug] targets This starts a bash with the same environment as the test framework uses, allowing easy debugging. Change-Id: I956deda913b73dae5b1e1976417834ae4731f88a Signed-off-by: Klement Sekera --- Makefile | 8 ++++++++ test/Makefile | 13 +++++++++++++ 2 files changed, 21 insertions(+) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 7faba3b6..f0173cc1 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,8 @@ help: @echo " test-debug - build and run (basic) functional tests (debug build)" @echo " test-all - build and run (all) functional tests" @echo " test-all-debug - build and run (all) functional tests (debug build)" + @echo " test-shell - enter shell with test environment" + @echo " test-shell-debug - enter shell with test environment (debug build)" @echo " test-wipe - wipe files generated by unit tests" @echo " retest - run functional tests" @echo " retest-debug - run functional tests (debug build)" @@ -247,6 +249,12 @@ test-help: test-wipe: @make -C test wipe +test-shell: bootstrap + $(call test,vpp_lite,vpp_lite,shell) + +test-shell-debug: bootstrap + $(call test,vpp_lite,vpp_lite_debug,shell) + test-doc: @make -C test doc diff --git a/test/Makefile b/test/Makefile index 1bb3c6c2..02149803 100644 --- a/test/Makefile +++ b/test/Makefile @@ -68,6 +68,17 @@ test: verify-python-path verify-no-running-vpp reset $(PAPI_INSTALL_DONE) retest: verify-python-path verify-no-running-vpp reset $(call retest-func) +shell: verify-python-path $(PAPI_INSTALL_DONE) + @echo "source $(PYTHON_VENV_PATH)/bin/activate;\ + echo '***';\ + echo VPP_TEST_BUILD_DIR=$(VPP_TEST_BUILD_DIR);\ + echo VPP_TEST_BIN=$(VPP_TEST_BIN);\ + echo VPP_TEST_PLUGIN_PATH=$(VPP_TEST_PLUGIN_PATH);\ + echo VPP_TEST_INSTALL_PATH=$(VPP_TEST_INSTALL_PATH);\ + echo LD_LIBRARY_PATH=$(LD_LIBRARY_PATH);\ + echo '***';\ + exec Date: Thu, 9 Mar 2017 08:20:13 +0100 Subject: make test: automatic sanity check Check if vpp_papi is importable before running the tests to avoid confusing python crashes. Change-Id: I6adf406e353bf381d590f2ef988a1ea79b95cf37 Signed-off-by: Klement Sekera --- test/Makefile | 16 ++++++++++++++-- test/sanity_import_vpp_papi.py | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 test/sanity_import_vpp_papi.py (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 02149803..8594633b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -62,10 +62,22 @@ define retest-func @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover $(UNITTEST_EXTRA_OPTS) -p test_\"*.py\"" endef -test: verify-python-path verify-no-running-vpp reset $(PAPI_INSTALL_DONE) +.PHONY: sanity + +sanity: verify-no-running-vpp + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python sanity_import_vpp_papi.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Sanity check failed, cannot import vpp_papi\" &&\ + echo \"* to debug: \" &&\ + echo \"* 1. enter test shell: make test-shell\" &&\ + echo \"* 2. execute debugger: gdb python -ex 'run sanity_import_vpp_papi.py'\" &&\ + echo \"*******************************************************************\" &&\ + false)" + +test: verify-python-path $(PAPI_INSTALL_DONE) sanity reset $(call retest-func) -retest: verify-python-path verify-no-running-vpp reset +retest: verify-python-path sanity reset $(call retest-func) shell: verify-python-path $(PAPI_INSTALL_DONE) diff --git a/test/sanity_import_vpp_papi.py b/test/sanity_import_vpp_papi.py new file mode 100644 index 00000000..535e00c9 --- /dev/null +++ b/test/sanity_import_vpp_papi.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +""" sanity check script """ +import vpp_papi -- cgit 1.2.3-korg From 3cc4971882235a539bc6177e8e4b4d92129b3a12 Mon Sep 17 00:00:00 2001 From: Ole Troan Date: Wed, 8 Mar 2017 12:02:24 +0100 Subject: Python API: Change from cPython to CFFI. Change-Id: I03e52466fb3f909ae52b8fba601168f3eadbd972 Signed-off-by: Ole Troan --- Makefile | 5 +- build-root/rpm/vpp.spec | 12 +- src/Makefile.am | 1 + src/configure.ac | 9 + src/vpp-api.am | 45 +++ src/vpp-api/pneum/pneum.c | 488 ++++++++++++++++++++++++ src/vpp-api/pneum/pneum.h | 37 ++ src/vpp-api/pneum/test_pneum.c | 143 +++++++ src/vpp-api/python/Makefile.am | 57 +-- src/vpp-api/python/pneum/pneum.c | 467 ----------------------- src/vpp-api/python/pneum/pneum.h | 34 -- src/vpp-api/python/pneum/test_pneum.c | 143 ------- src/vpp-api/python/setup.cfg | 2 +- src/vpp-api/python/setup.py | 15 +- src/vpp-api/python/vpp_papi.py | 635 +++++++++++++++++++++++++++++++ src/vpp-api/python/vpp_papi/__init__.py | 3 - src/vpp-api/python/vpp_papi/pneum_wrap.c | 226 ----------- src/vpp-api/python/vpp_papi/vpp_papi.py | 599 ----------------------------- test/Makefile | 2 +- test/vpp_papi_provider.py | 2 +- 20 files changed, 1380 insertions(+), 1545 deletions(-) create mode 100644 src/vpp-api.am create mode 100644 src/vpp-api/pneum/pneum.c create mode 100644 src/vpp-api/pneum/pneum.h create mode 100644 src/vpp-api/pneum/test_pneum.c delete mode 100644 src/vpp-api/python/pneum/pneum.c delete mode 100644 src/vpp-api/python/pneum/pneum.h delete mode 100644 src/vpp-api/python/pneum/test_pneum.c create mode 100644 src/vpp-api/python/vpp_papi.py delete mode 100644 src/vpp-api/python/vpp_papi/__init__.py delete mode 100644 src/vpp-api/python/vpp_papi/pneum_wrap.c delete mode 100644 src/vpp-api/python/vpp_papi/vpp_papi.py (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index ebf94db7..f47c98a5 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,8 @@ endif DEB_DEPENDS = curl build-essential autoconf automake bison libssl-dev ccache DEB_DEPENDS += debhelper dkms git libtool libganglia1-dev libapr1-dev dh-systemd DEB_DEPENDS += libconfuse-dev git-review exuberant-ctags cscope pkg-config -DEB_DEPENDS += python-dev python-virtualenv python-pip lcov chrpath autoconf nasm +DEB_DEPENDS += lcov chrpath autoconf nasm +DEB_DEPENDS += python-dev python-virtualenv python-pip libffi6 ifeq ($(OS_VERSION_ID),14.04) DEB_DEPENDS += openjdk-8-jdk-headless else @@ -49,7 +50,7 @@ endif RPM_DEPENDS_GROUPS = 'Development Tools' RPM_DEPENDS = redhat-lsb glibc-static java-1.8.0-openjdk-devel yum-utils RPM_DEPENDS += openssl-devel https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm apr-devel -RPM_DEPENDS += python-devel python-virtualenv lcov chrpath +RPM_DEPENDS += python-devel python-virtualenv lcov chrpath libffi-devel RPM_DEPENDS += https://kojipkgs.fedoraproject.org//packages/nasm/2.12.02/2.fc26/x86_64/nasm-2.12.02-2.fc26.x86_64.rpm EPEL_DEPENDS = libconfuse-devel ganglia-devel diff --git a/build-root/rpm/vpp.spec b/build-root/rpm/vpp.spec index 7bc18ca4..7fa97888 100644 --- a/build-root/rpm/vpp.spec +++ b/build-root/rpm/vpp.spec @@ -162,8 +162,8 @@ do done # Python bindings -mkdir -p -m755 %{buildroot}%{python2_sitelib} -install -p -m 666 %{_mu_build_dir}/%{_vpp_install_dir}/*/lib/python2.7/site-packages/vpp_papi-*.egg %{buildroot}%{python2_sitelib} +cd %{_mu_build_dir}/../src/vpp-api/python +%py2_install # # devel @@ -226,15 +226,9 @@ done sysctl --system %systemd_post vpp.service -%post api-python -easy_install -z %{python2_sitelib}/vpp_papi-*.egg - %preun %systemd_preun vpp.service -%preun api-python -easy_install -mxNq vpp_papi - %postun %systemd_postun @@ -285,7 +279,7 @@ fi %files api-python %defattr(644,root,root) -%{python2_sitelib}/vpp_papi-*.egg +%{python2_sitelib}/* %files devel %defattr(-,bin,bin) diff --git a/src/Makefile.am b/src/Makefile.am index 5daaa48e..41076e0e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -72,6 +72,7 @@ include uri.am SUBDIRS += plugins if ENABLE_PAPI +include vpp-api.am SUBDIRS += vpp-api/python endif diff --git a/src/configure.ac b/src/configure.ac index d90740d9..5e02adc1 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -201,6 +201,15 @@ AM_COND_IF([ENABLE_JAPI], AC_SUBST(JAR) ]) +############################################################################### +# PYTHON +############################################################################### + +AM_COND_IF([ENABLE_PAPI], +[ + AM_PATH_PYTHON +]) + ############################################################################### # Output ############################################################################### diff --git a/src/vpp-api.am b/src/vpp-api.am new file mode 100644 index 00000000..0e05d60d --- /dev/null +++ b/src/vpp-api.am @@ -0,0 +1,45 @@ +# Copyright (c) 2017 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# VPP API C wrapper extension +# +lib_LTLIBRARIES += libpneum.la +libpneum_la_SOURCES = vpp-api/pneum/pneum.c +libpneum_la_LIBADD = \ + $(top_builddir)/libvppinfra.la \ + $(top_builddir)/libvlibmemoryclient.la \ + $(top_builddir)/libvlibapi.la \ + $(top_builddir)/libsvm.la \ + -lpthread -lm -lrt + +libpneum_la_LDFLAGS = -module +libpneum_la_CPPFLAGS = + +nobase_include_HEADERS += vpp-api/pneum/pneum.h + +# +# Test client +# +if ENABLE_TESTS +noinst_PROGRAMS += test_pneum +test_pneum_SOURCES = vpp-api/pneum/pneum.c vpp-api/pneum/test_pneum.c +test_pneum_LDADD = \ + $(top_builddir)/libvppinfra.la \ + $(top_builddir)/libvlibmemoryclient.la \ + $(top_builddir)/libvlibapi.la \ + $(top_builddir)/libsvm.la \ + -lpthread -lm -lrt +endif + +# vi:syntax=automake diff --git a/src/vpp-api/pneum/pneum.c b/src/vpp-api/pneum/pneum.c new file mode 100644 index 00000000..cbae5cff --- /dev/null +++ b/src/vpp-api/pneum/pneum.c @@ -0,0 +1,488 @@ +/* + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "pneum.h" + +/* + * Asynchronous mode: + * Client registers a callback. All messages are sent to the callback. + * Synchronous mode: + * Client calls blocking read(). + * Clients are expected to collate events on a queue. + * pneum_write() -> suspends RX thread + * pneum_read() -> resumes RX thread + */ + +#define vl_typedefs /* define message structures */ +#include +#undef vl_typedefs + +#define vl_endianfun /* define message structures */ +#include +#undef vl_endianfun + +vlib_main_t vlib_global_main; +vlib_main_t **vlib_mains; + +typedef struct { + u8 connected_to_vlib; + pthread_t rx_thread_handle; + pthread_t timeout_thread_handle; + pthread_mutex_t queue_lock; + pthread_cond_t suspend_cv; + pthread_cond_t resume_cv; + pthread_mutex_t timeout_lock; + pthread_cond_t timeout_cv; + pthread_cond_t timeout_cancel_cv; + pthread_cond_t terminate_cv; +} pneum_main_t; + +pneum_main_t pneum_main; +pneum_callback_t pneum_callback; +u16 read_timeout = 0; +bool rx_is_running = false; + +static void +init (void) +{ + pneum_main_t *pm = &pneum_main; + memset(pm, 0, sizeof(*pm)); + pthread_mutex_init(&pm->queue_lock, NULL); + pthread_cond_init(&pm->suspend_cv, NULL); + pthread_cond_init(&pm->resume_cv, NULL); + pthread_mutex_init(&pm->timeout_lock, NULL); + pthread_cond_init(&pm->timeout_cv, NULL); + pthread_cond_init(&pm->timeout_cancel_cv, NULL); + pthread_cond_init(&pm->terminate_cv, NULL); +} + +static void +cleanup (void) +{ + pneum_main_t *pm = &pneum_main; + pthread_cond_destroy(&pm->suspend_cv); + pthread_cond_destroy(&pm->resume_cv); + pthread_cond_destroy(&pm->timeout_cv); + pthread_cond_destroy(&pm->timeout_cancel_cv); + pthread_cond_destroy(&pm->terminate_cv); + pthread_mutex_destroy(&pm->queue_lock); + pthread_mutex_destroy(&pm->timeout_lock); + memset (pm, 0, sizeof (*pm)); +} + +/* + * Satisfy external references when -lvlib is not available. + */ +void vlib_cli_output (struct vlib_main_t * vm, char * fmt, ...) +{ + clib_warning ("vlib_cli_output called..."); +} + +void +pneum_free (void * msg) +{ + vl_msg_api_free (msg); +} + +static void +pneum_api_handler (void *msg) +{ + u16 id = ntohs(*((u16 *)msg)); + msgbuf_t *msgbuf = (msgbuf_t *)(((u8 *)msg) - offsetof(msgbuf_t, data)); + int l = ntohl(msgbuf->data_len); + if (l == 0) + clib_warning("Message ID %d has wrong length: %d\n", id, l); + + /* Call Python callback */ + ASSERT(pneum_callback); + (pneum_callback)(msg, l); + pneum_free(msg); +} + +static void * +pneum_rx_thread_fn (void *arg) +{ + unix_shared_memory_queue_t *q; + pneum_main_t *pm = &pneum_main; + api_main_t *am = &api_main; + uword msg; + + q = am->vl_input_queue; + + while (1) + while (!unix_shared_memory_queue_sub(q, (u8 *)&msg, 0)) + { + u16 id = ntohs(*((u16 *)msg)); + switch (id) { + case VL_API_RX_THREAD_EXIT: + vl_msg_api_free((void *) msg); + /* signal waiting threads that this thread is about to terminate */ + pthread_mutex_lock(&pm->queue_lock); + pthread_cond_signal(&pm->terminate_cv); + pthread_mutex_unlock(&pm->queue_lock); + pthread_exit(0); + return 0; + break; + + case VL_API_MEMCLNT_RX_THREAD_SUSPEND: + vl_msg_api_free((void * )msg); + /* Suspend thread and signal reader */ + pthread_mutex_lock(&pm->queue_lock); + pthread_cond_signal(&pm->suspend_cv); + /* Wait for the resume signal */ + pthread_cond_wait (&pm->resume_cv, &pm->queue_lock); + pthread_mutex_unlock(&pm->queue_lock); + break; + + case VL_API_MEMCLNT_READ_TIMEOUT: + clib_warning("Received read timeout in async thread\n"); + vl_msg_api_free((void *) msg); + break; + + default: + pneum_api_handler((void *)msg); + } + } +} + +static void * +pneum_timeout_thread_fn (void *arg) +{ + vl_api_memclnt_read_timeout_t *ep; + pneum_main_t *pm = &pneum_main; + api_main_t *am = &api_main; + struct timespec ts; + struct timeval tv; + u16 timeout; + int rv; + + while (1) + { + /* Wait for poke */ + pthread_mutex_lock(&pm->timeout_lock); + pthread_cond_wait (&pm->timeout_cv, &pm->timeout_lock); + timeout = read_timeout; + gettimeofday(&tv, NULL); + ts.tv_sec = tv.tv_sec + timeout; + ts.tv_nsec = 0; + rv = pthread_cond_timedwait (&pm->timeout_cancel_cv, + &pm->timeout_lock, &ts); + pthread_mutex_unlock(&pm->timeout_lock); + if (rv == ETIMEDOUT) + { + ep = vl_msg_api_alloc (sizeof (*ep)); + ep->_vl_msg_id = ntohs(VL_API_MEMCLNT_READ_TIMEOUT); + vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); + } + } + pthread_exit(0); +} + +void +pneum_rx_suspend (void) +{ + api_main_t *am = &api_main; + pneum_main_t *pm = &pneum_main; + vl_api_memclnt_rx_thread_suspend_t *ep; + + if (!pm->rx_thread_handle) return; + pthread_mutex_lock(&pm->queue_lock); + if (rx_is_running) + { + ep = vl_msg_api_alloc (sizeof (*ep)); + ep->_vl_msg_id = ntohs(VL_API_MEMCLNT_RX_THREAD_SUSPEND); + vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); + /* Wait for RX thread to tell us it has suspendend */ + pthread_cond_wait(&pm->suspend_cv, &pm->queue_lock); + rx_is_running = false; + } + pthread_mutex_unlock(&pm->queue_lock); +} + +void +pneum_rx_resume (void) +{ + pneum_main_t *pm = &pneum_main; + if (!pm->rx_thread_handle) return; + pthread_mutex_lock(&pm->queue_lock); + if (rx_is_running) return; + pthread_cond_signal(&pm->resume_cv); + rx_is_running = true; + pthread_mutex_unlock(&pm->queue_lock); +} + +static uword * +pneum_msg_table_get_hash (void) +{ + api_main_t *am = &api_main; + return (am->msg_index_by_name_and_crc); +} + +int +pneum_msg_table_size(void) +{ + api_main_t *am = &api_main; + return hash_elts(am->msg_index_by_name_and_crc); +} + +int +pneum_connect (char * name, char * chroot_prefix, pneum_callback_t cb, + int rx_qlen) +{ + int rv = 0; + pneum_main_t *pm = &pneum_main; + + init(); + if (chroot_prefix != NULL) + vl_set_memory_root_path (chroot_prefix); + + if ((rv = vl_client_api_map("/vpe-api"))) { + clib_warning ("vl_client_api map rv %d", rv); + return rv; + } + + if (vl_client_connect(name, 0, rx_qlen) < 0) { + vl_client_api_unmap(); + return (-1); + } + + if (cb) { + /* Start the rx queue thread */ + rv = pthread_create(&pm->rx_thread_handle, NULL, pneum_rx_thread_fn, 0); + if (rv) { + clib_warning("pthread_create returned %d", rv); + vl_client_api_unmap(); + return (-1); + } + pneum_callback = cb; + rx_is_running = true; + } + + /* Start read timeout thread */ + rv = pthread_create(&pm->timeout_thread_handle, NULL, + pneum_timeout_thread_fn, 0); + if (rv) { + clib_warning("pthread_create returned %d", rv); + vl_client_api_unmap(); + return (-1); + } + + pm->connected_to_vlib = 1; + + return (0); +} + +int +pneum_disconnect (void) +{ + api_main_t *am = &api_main; + pneum_main_t *pm = &pneum_main; + + if (!pm->connected_to_vlib) return 0; + + if (pm->rx_thread_handle) { + vl_api_rx_thread_exit_t *ep; + uword junk; + ep = vl_msg_api_alloc (sizeof (*ep)); + ep->_vl_msg_id = ntohs(VL_API_RX_THREAD_EXIT); + vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); + + /* wait (with timeout) until RX thread has finished */ + struct timespec ts; + struct timeval tv; + gettimeofday(&tv, NULL); + ts.tv_sec = tv.tv_sec + 5; + ts.tv_nsec = 0; + pthread_mutex_lock(&pm->queue_lock); + int rv = pthread_cond_timedwait(&pm->terminate_cv, &pm->queue_lock, &ts); + pthread_mutex_unlock(&pm->queue_lock); + /* now join so we wait until thread has -really- finished */ + if (rv == ETIMEDOUT) + pthread_cancel(pm->rx_thread_handle); + else + pthread_join(pm->rx_thread_handle, (void **) &junk); + } + if (pm->timeout_thread_handle) + pthread_cancel(pm->timeout_thread_handle); + + vl_client_disconnect(); + vl_client_api_unmap(); + pneum_callback = 0; + + cleanup(); + + return (0); +} + +static void +set_timeout (unsigned short timeout) +{ + pneum_main_t *pm = &pneum_main; + pthread_mutex_lock(&pm->timeout_lock); + read_timeout = timeout; + pthread_cond_signal(&pm->timeout_cv); + pthread_mutex_unlock(&pm->timeout_lock); +} + +static void +unset_timeout (void) +{ + pneum_main_t *pm = &pneum_main; + pthread_mutex_lock(&pm->timeout_lock); + pthread_cond_signal(&pm->timeout_cancel_cv); + pthread_mutex_unlock(&pm->timeout_lock); +} + +int +pneum_read (char **p, int *l, u16 timeout) +{ + unix_shared_memory_queue_t *q; + api_main_t *am = &api_main; + pneum_main_t *pm = &pneum_main; + uword msg; + msgbuf_t *msgbuf; + + if (!pm->connected_to_vlib) return -1; + + *l = 0; + + if (am->our_pid == 0) return (-1); + + /* Poke timeout thread */ + if (timeout) + set_timeout(timeout); + + q = am->vl_input_queue; + int rv = unix_shared_memory_queue_sub(q, (u8 *)&msg, 0); + if (rv == 0) { + u16 msg_id = ntohs(*((u16 *)msg)); + switch (msg_id) { + case VL_API_RX_THREAD_EXIT: + printf("Received thread exit\n"); + return -1; + case VL_API_MEMCLNT_RX_THREAD_SUSPEND: + printf("Received thread suspend\n"); + goto error; + case VL_API_MEMCLNT_READ_TIMEOUT: + printf("Received read timeout %ds\n", timeout); + goto error; + + default: + msgbuf = (msgbuf_t *)(((u8 *)msg) - offsetof(msgbuf_t, data)); + *l = ntohl(msgbuf->data_len); + if (*l == 0) { + printf("Unregistered API message: %d\n", msg_id); + goto error; + } + } + *p = (char *)msg; + + /* Let timeout notification thread know we're done */ + unset_timeout(); + + } else { + printf("Read failed with %d\n", rv); + } + return (rv); + + error: + vl_msg_api_free((void *) msg); + /* Client might forget to resume RX thread on failure */ + pneum_rx_resume (); + return -1; +} + +/* + * XXX: Makes the assumption that client_index is the first member + */ +typedef VL_API_PACKED(struct _vl_api_header { + u16 _vl_msg_id; + u32 client_index; +}) vl_api_header_t; + +static unsigned int +pneum_client_index (void) +{ + return (api_main.my_client_index); +} + +int +pneum_write (char *p, int l) +{ + int rv = -1; + api_main_t *am = &api_main; + vl_api_header_t *mp = vl_msg_api_alloc(l); + unix_shared_memory_queue_t *q; + pneum_main_t *pm = &pneum_main; + + if (!pm->connected_to_vlib) return -1; + if (!mp) return (-1); + + memcpy(mp, p, l); + mp->client_index = pneum_client_index(); + q = am->shmem_hdr->vl_input_queue; + rv = unix_shared_memory_queue_add(q, (u8 *)&mp, 0); + if (rv != 0) { + clib_warning("vpe_api_write fails: %d\n", rv); + /* Clear message */ + pneum_free(mp); + } + return (rv); +} + +int +pneum_get_msg_index (unsigned char * name) +{ + return vl_api_get_msg_index (name); +} + +int +pneum_msg_table_max_index(void) +{ + int max = 0; + hash_pair_t *hp; + uword *h = pneum_msg_table_get_hash(); + hash_foreach_pair (hp, h, + ({ + if (hp->value[0] > max) + max = hp->value[0]; + })); + + return max; +} + +void +pneum_set_error_handler (pneum_error_callback_t cb) +{ + if (cb) clib_error_register_handler (cb, 0); +} diff --git a/src/vpp-api/pneum/pneum.h b/src/vpp-api/pneum/pneum.h new file mode 100644 index 00000000..669298df --- /dev/null +++ b/src/vpp-api/pneum/pneum.h @@ -0,0 +1,37 @@ +/* + * 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. + */ +#ifndef included_pneum_h +#define included_pneum_h + +#include +#include + +typedef void (*pneum_callback_t)(unsigned char * data, int len); +typedef void (*pneum_error_callback_t)(void *, unsigned char *, int); +int pneum_connect(char * name, char * chroot_prefix, pneum_callback_t cb, + int rx_qlen); +int pneum_disconnect(void); +int pneum_read(char **data, int *l, unsigned short timeout); +int pneum_write(char *data, int len); +void pneum_free(void * msg); + +int pneum_get_msg_index(unsigned char * name); +int pneum_msg_table_size(void); +int pneum_msg_table_max_index(void); + +void pneum_rx_suspend (void); +void pneum_rx_resume (void); +void pneum_set_error_handler(pneum_error_callback_t); +#endif diff --git a/src/vpp-api/pneum/test_pneum.c b/src/vpp-api/pneum/test_pneum.c new file mode 100644 index 00000000..334e58e9 --- /dev/null +++ b/src/vpp-api/pneum/test_pneum.c @@ -0,0 +1,143 @@ +/* + *------------------------------------------------------------------ + * test_pneum.c + * + * 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. + *------------------------------------------------------------------ + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include /* time_t, time (for timestamp in second) */ +#include /* ftime, timeb (for timestamp in millisecond) */ +#include /* gettimeofday, timeval (for timestamp in microsecond) */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "pneum.h" + +#define vl_typedefs /* define message structures */ +#include +#undef vl_typedefs + +/* we are not linking with vlib */ +vlib_main_t vlib_global_main; +vlib_main_t **vlib_mains; + +volatile int sigterm_received = 0; +volatile u32 result_ready; +volatile u16 result_msg_id; + +/* M_NOALLOC: construct, but don't yet send a message */ + +#define M_NOALLOC(T,t) \ + do { \ + result_ready = 0; \ + memset (mp, 0, sizeof (*mp)); \ + mp->_vl_msg_id = ntohs (VL_API_##T); \ + mp->client_index = am->my_client_index; \ + } while(0); + + + +int +wrap_pneum_callback (char *data, int len) +{ + //printf("Callback %d\n", len); + result_ready = 1; + result_msg_id = ntohs(*((u16 *)data)); + return (0); +} + +int main (int argc, char ** argv) +{ + api_main_t * am = &api_main; + vl_api_show_version_t message; + vl_api_show_version_t *mp; + int async = 1; + int rv = pneum_connect("pneum_client", NULL, NULL, 32 /* rx queue-length*/); + + if (rv != 0) { + printf("Connect failed: %d\n", rv); + exit(rv); + } + + struct timeb timer_msec; + long long int timestamp_msec_start; /* timestamp in millisecond. */ + if (!ftime(&timer_msec)) { + timestamp_msec_start = ((long long int) timer_msec.time) * 1000ll + + (long long int) timer_msec.millitm; + } + else { + timestamp_msec_start = -1; + } + + + /* + * Test vpe_api_write and vpe_api_read to send and recv message for an + * API + */ + int i; + long int no_msgs = 10000; + mp = &message; + + for (i = 0; i < no_msgs; i++) { + /* Construct the API message */ + M_NOALLOC(SHOW_VERSION, show_version); + pneum_write((char *)mp, sizeof(*mp)); +#ifndef __COVERITY__ + /* As given, async is always 1. Shut up Coverity about it */ + if (!async) + while (result_ready == 0); +#endif + } + if (async) { + vl_api_control_ping_t control; + vl_api_control_ping_t *mp; + mp = &control; + M_NOALLOC(CONTROL_PING, control_ping); + pneum_write((char *)mp, sizeof(*mp)); + + while (result_msg_id != VL_API_CONTROL_PING_REPLY); + } + + long long int timestamp_msec_end; /* timestamp in millisecond. */ + if (!ftime(&timer_msec)) { + timestamp_msec_end = ((long long int) timer_msec.time) * 1000ll + + (long long int) timer_msec.millitm; + } + else { + timestamp_msec_end = -1; + } + + printf("Took %lld msec, %lld msgs/msec \n", (timestamp_msec_end - timestamp_msec_start), + no_msgs/(timestamp_msec_end - timestamp_msec_start)); + fformat(stdout, "Exiting...\n"); + pneum_disconnect(); + exit (0); +} diff --git a/src/vpp-api/python/Makefile.am b/src/vpp-api/python/Makefile.am index 54076822..6f5beb69 100644 --- a/src/vpp-api/python/Makefile.am +++ b/src/vpp-api/python/Makefile.am @@ -11,52 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -AUTOMAKE_OPTIONS = foreign -ACLOCAL_AMFLAGS = -I m4 -AM_LIBTOOLFLAGS = --quiet -AM_CFLAGS = -Wall -I${top_srcdir} -I${top_builddir} - -BUILT_SOURCES = -bin_PROGRAMS = -CLEANFILES = -lib_LTLIBRARIES = -noinst_PROGRAMS = -nobase_include_HEADERS = pneum/pneum.h - -# -# Python / C extension -# -lib_LTLIBRARIES += libpneum.la -libpneum_la_SOURCES = pneum/pneum.c -libpneum_la_LIBADD = \ - $(top_builddir)/libvppinfra.la \ - $(top_builddir)/libvlibmemoryclient.la \ - $(top_builddir)/libvlibapi.la \ - $(top_builddir)/libsvm.la \ - -lpthread -lm -lrt - -libpneum_la_LDFLAGS = -module -libpneum_la_CPPFLAGS = - -# TODO: Support both Python 2 and 3. -install-exec-local: $(lib_LTLIBRARIES) - cd $(srcdir); \ - mkdir -p $(pythondir); \ - mkdir -p $(pyexecdir); \ - PYTHONUSERBASE=$(prefix) \ - python setup.py build_ext -L $(libdir) \ - -I $(prefix)/include/ install --user - -# -# Test client -# -if ENABLE_TESTS -noinst_PROGRAMS += test_pneum -test_pneum_SOURCES = pneum/pneum.c pneum/test_pneum.c -test_pneum_LDADD = \ - $(top_builddir)/libvppinfra.la \ - $(top_builddir)/libvlibmemoryclient.la \ - $(top_builddir)/libvlibapi.la \ - $(top_builddir)/libsvm.la \ - -lpthread -lm -lrt -endif +install-exec-local: + (cd $(srcdir) ; $(PYTHON) $(srcdir)/setup.py build \ + --build-base $(shell readlink -f $(builddir))/build \ + install \ + --root / \ + --prefix $(DESTDIR)$(prefix) \ + --single-version-externally-managed \ + --verbose) diff --git a/src/vpp-api/python/pneum/pneum.c b/src/vpp-api/python/pneum/pneum.c deleted file mode 100644 index da9d69df..00000000 --- a/src/vpp-api/python/pneum/pneum.c +++ /dev/null @@ -1,467 +0,0 @@ -/* - * 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. - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "pneum.h" - -/* - * Asynchronous mode: - * Client registers a callback. All messages are sent to the callback. - * Synchronous mode: - * Client calls blocking read(). - * Clients are expected to collate events on a queue. - * pneum_write() -> suspends RX thread - * pneum_read() -> resumes RX thread - */ - -#define vl_typedefs /* define message structures */ -#include -#undef vl_typedefs - -#define vl_endianfun /* define message structures */ -#include -#undef vl_endianfun - -vlib_main_t vlib_global_main; -vlib_main_t **vlib_mains; - -typedef struct { - u8 connected_to_vlib; - pthread_t rx_thread_handle; - pthread_t timeout_thread_handle; - pthread_mutex_t queue_lock; - pthread_cond_t suspend_cv; - pthread_cond_t resume_cv; - pthread_mutex_t timeout_lock; - pthread_cond_t timeout_cv; - pthread_cond_t timeout_cancel_cv; - pthread_cond_t terminate_cv; -} pneum_main_t; - -pneum_main_t pneum_main; -pneum_callback_t pneum_callback; -u16 read_timeout = 0; -bool rx_is_running = false; - -static void -init (void) -{ - pneum_main_t *pm = &pneum_main; - memset(pm, 0, sizeof(*pm)); - pthread_mutex_init(&pm->queue_lock, NULL); - pthread_cond_init(&pm->suspend_cv, NULL); - pthread_cond_init(&pm->resume_cv, NULL); - pthread_mutex_init(&pm->timeout_lock, NULL); - pthread_cond_init(&pm->timeout_cv, NULL); - pthread_cond_init(&pm->timeout_cancel_cv, NULL); - pthread_cond_init(&pm->terminate_cv, NULL); -} - -static void -cleanup (void) -{ - pneum_main_t *pm = &pneum_main; - pthread_cond_destroy(&pm->suspend_cv); - pthread_cond_destroy(&pm->resume_cv); - pthread_cond_destroy(&pm->timeout_cv); - pthread_cond_destroy(&pm->timeout_cancel_cv); - pthread_cond_destroy(&pm->terminate_cv); - pthread_mutex_destroy(&pm->queue_lock); - pthread_mutex_destroy(&pm->timeout_lock); - memset (pm, 0, sizeof (*pm)); -} - -/* - * Satisfy external references when -lvlib is not available. - */ -void vlib_cli_output (struct vlib_main_t * vm, char * fmt, ...) -{ - clib_warning ("vlib_cli_output called..."); -} - -void -pneum_free (void * msg) -{ - vl_msg_api_free (msg); -} - -static void -pneum_api_handler (void *msg) -{ - u16 id = ntohs(*((u16 *)msg)); - msgbuf_t *msgbuf = (msgbuf_t *)(((u8 *)msg) - offsetof(msgbuf_t, data)); - int l = ntohl(msgbuf->data_len); - if (l == 0) - clib_warning("Message ID %d has wrong length: %d\n", id, l); - - /* Call Python callback */ - ASSERT(pneum_callback); - (pneum_callback)(msg, l); - pneum_free(msg); -} - -static void * -pneum_rx_thread_fn (void *arg) -{ - unix_shared_memory_queue_t *q; - pneum_main_t *pm = &pneum_main; - api_main_t *am = &api_main; - uword msg; - - q = am->vl_input_queue; - - while (1) - while (!unix_shared_memory_queue_sub(q, (u8 *)&msg, 0)) - { - u16 id = ntohs(*((u16 *)msg)); - switch (id) { - case VL_API_RX_THREAD_EXIT: - vl_msg_api_free((void *) msg); - /* signal waiting threads that this thread is about to terminate */ - pthread_mutex_lock(&pm->queue_lock); - pthread_cond_signal(&pm->terminate_cv); - pthread_mutex_unlock(&pm->queue_lock); - pthread_exit(0); - return 0; - break; - - case VL_API_MEMCLNT_RX_THREAD_SUSPEND: - vl_msg_api_free((void * )msg); - /* Suspend thread and signal reader */ - pthread_mutex_lock(&pm->queue_lock); - pthread_cond_signal(&pm->suspend_cv); - /* Wait for the resume signal */ - pthread_cond_wait (&pm->resume_cv, &pm->queue_lock); - pthread_mutex_unlock(&pm->queue_lock); - break; - - case VL_API_MEMCLNT_READ_TIMEOUT: - clib_warning("Received read timeout in async thread\n"); - vl_msg_api_free((void *) msg); - break; - - default: - pneum_api_handler((void *)msg); - } - } -} - -static void * -pneum_timeout_thread_fn (void *arg) -{ - vl_api_memclnt_read_timeout_t *ep; - pneum_main_t *pm = &pneum_main; - api_main_t *am = &api_main; - struct timespec ts; - struct timeval tv; - u16 timeout; - int rv; - - while (1) - { - /* Wait for poke */ - pthread_mutex_lock(&pm->timeout_lock); - pthread_cond_wait (&pm->timeout_cv, &pm->timeout_lock); - timeout = read_timeout; - gettimeofday(&tv, NULL); - ts.tv_sec = tv.tv_sec + timeout; - ts.tv_nsec = 0; - rv = pthread_cond_timedwait (&pm->timeout_cancel_cv, - &pm->timeout_lock, &ts); - pthread_mutex_unlock(&pm->timeout_lock); - if (rv == ETIMEDOUT) - { - ep = vl_msg_api_alloc (sizeof (*ep)); - ep->_vl_msg_id = ntohs(VL_API_MEMCLNT_READ_TIMEOUT); - vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); - } - } - pthread_exit(0); -} - -void -pneum_rx_suspend (void) -{ - api_main_t *am = &api_main; - pneum_main_t *pm = &pneum_main; - vl_api_memclnt_rx_thread_suspend_t *ep; - - if (!pm->rx_thread_handle) return; - pthread_mutex_lock(&pm->queue_lock); - if (rx_is_running) - { - ep = vl_msg_api_alloc (sizeof (*ep)); - ep->_vl_msg_id = ntohs(VL_API_MEMCLNT_RX_THREAD_SUSPEND); - vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); - /* Wait for RX thread to tell us it has suspendend */ - pthread_cond_wait(&pm->suspend_cv, &pm->queue_lock); - rx_is_running = false; - } - pthread_mutex_unlock(&pm->queue_lock); -} - -void -pneum_rx_resume (void) -{ - pneum_main_t *pm = &pneum_main; - if (!pm->rx_thread_handle) return; - pthread_mutex_lock(&pm->queue_lock); - if (rx_is_running) return; - pthread_cond_signal(&pm->resume_cv); - rx_is_running = true; - pthread_mutex_unlock(&pm->queue_lock); -} - -uword * -pneum_msg_table_get_hash (void) -{ - api_main_t *am = &api_main; - return (am->msg_index_by_name_and_crc); -} - -int -pneum_msg_table_size(void) -{ - api_main_t *am = &api_main; - return hash_elts(am->msg_index_by_name_and_crc); -} - -int -pneum_connect (char * name, char * chroot_prefix, pneum_callback_t cb, - int rx_qlen) -{ - int rv = 0; - pneum_main_t *pm = &pneum_main; - - init(); - if (chroot_prefix != NULL) - vl_set_memory_root_path (chroot_prefix); - - if ((rv = vl_client_api_map("/vpe-api"))) { - clib_warning ("vl_client_api map rv %d", rv); - return rv; - } - - if (vl_client_connect(name, 0, rx_qlen) < 0) { - vl_client_api_unmap(); - return (-1); - } - - if (cb) { - /* Start the rx queue thread */ - rv = pthread_create(&pm->rx_thread_handle, NULL, pneum_rx_thread_fn, 0); - if (rv) { - clib_warning("pthread_create returned %d", rv); - vl_client_api_unmap(); - return (-1); - } - pneum_callback = cb; - rx_is_running = true; - } - - /* Start read timeout thread */ - rv = pthread_create(&pm->timeout_thread_handle, NULL, - pneum_timeout_thread_fn, 0); - if (rv) { - clib_warning("pthread_create returned %d", rv); - vl_client_api_unmap(); - return (-1); - } - - pm->connected_to_vlib = 1; - - return (0); -} - -int -pneum_disconnect (void) -{ - api_main_t *am = &api_main; - pneum_main_t *pm = &pneum_main; - - if (!pm->connected_to_vlib) return 0; - - if (pm->rx_thread_handle) { - vl_api_rx_thread_exit_t *ep; - uword junk; - ep = vl_msg_api_alloc (sizeof (*ep)); - ep->_vl_msg_id = ntohs(VL_API_RX_THREAD_EXIT); - vl_msg_api_send_shmem(am->vl_input_queue, (u8 *)&ep); - - /* wait (with timeout) until RX thread has finished */ - struct timespec ts; - struct timeval tv; - gettimeofday(&tv, NULL); - ts.tv_sec = tv.tv_sec + 5; - ts.tv_nsec = 0; - pthread_mutex_lock(&pm->queue_lock); - int rv = pthread_cond_timedwait(&pm->terminate_cv, &pm->queue_lock, &ts); - pthread_mutex_unlock(&pm->queue_lock); - /* now join so we wait until thread has -really- finished */ - if (rv == ETIMEDOUT) - pthread_cancel(pm->rx_thread_handle); - else - pthread_join(pm->rx_thread_handle, (void **) &junk); - } - if (pm->timeout_thread_handle) - pthread_cancel(pm->timeout_thread_handle); - - vl_client_disconnect(); - vl_client_api_unmap(); - pneum_callback = 0; - - cleanup(); - - return (0); -} - -static void -set_timeout (unsigned short timeout) -{ - pneum_main_t *pm = &pneum_main; - pthread_mutex_lock(&pm->timeout_lock); - read_timeout = timeout; - pthread_cond_signal(&pm->timeout_cv); - pthread_mutex_unlock(&pm->timeout_lock); -} - -static void -unset_timeout (void) -{ - pneum_main_t *pm = &pneum_main; - pthread_mutex_lock(&pm->timeout_lock); - pthread_cond_signal(&pm->timeout_cancel_cv); - pthread_mutex_unlock(&pm->timeout_lock); -} - -int -pneum_read (char **p, int *l, u16 timeout) -{ - unix_shared_memory_queue_t *q; - api_main_t *am = &api_main; - pneum_main_t *pm = &pneum_main; - uword msg; - msgbuf_t *msgbuf; - - if (!pm->connected_to_vlib) return -1; - - *l = 0; - - if (am->our_pid == 0) return (-1); - - /* Poke timeout thread */ - if (timeout) - set_timeout(timeout); - - q = am->vl_input_queue; - int rv = unix_shared_memory_queue_sub(q, (u8 *)&msg, 0); - if (rv == 0) { - u16 msg_id = ntohs(*((u16 *)msg)); - switch (msg_id) { - case VL_API_RX_THREAD_EXIT: - printf("Received thread exit\n"); - return -1; - case VL_API_MEMCLNT_RX_THREAD_SUSPEND: - printf("Received thread suspend\n"); - goto error; - case VL_API_MEMCLNT_READ_TIMEOUT: - printf("Received read timeout %ds\n", timeout); - goto error; - - default: - msgbuf = (msgbuf_t *)(((u8 *)msg) - offsetof(msgbuf_t, data)); - *l = ntohl(msgbuf->data_len); - if (*l == 0) { - printf("Unregistered API message: %d\n", msg_id); - goto error; - } - } - *p = (char *)msg; - - /* Let timeout notification thread know we're done */ - unset_timeout(); - - } else { - printf("Read failed with %d\n", rv); - } - return (rv); - - error: - vl_msg_api_free((void *) msg); - /* Client might forget to resume RX thread on failure */ - pneum_rx_resume (); - return -1; -} - -/* - * XXX: Makes the assumption that client_index is the first member - */ -typedef VL_API_PACKED(struct _vl_api_header { - u16 _vl_msg_id; - u32 client_index; -}) vl_api_header_t; - -static unsigned int -pneum_client_index (void) -{ - return (api_main.my_client_index); -} - -int -pneum_write (char *p, int l) -{ - int rv = -1; - api_main_t *am = &api_main; - vl_api_header_t *mp = vl_msg_api_alloc(l); - unix_shared_memory_queue_t *q; - pneum_main_t *pm = &pneum_main; - - if (!pm->connected_to_vlib) return -1; - if (!mp) return (-1); - - memcpy(mp, p, l); - mp->client_index = pneum_client_index(); - q = am->shmem_hdr->vl_input_queue; - rv = unix_shared_memory_queue_add(q, (u8 *)&mp, 0); - if (rv != 0) { - clib_warning("vpe_api_write fails: %d\n", rv); - /* Clear message */ - pneum_free(mp); - } - return (rv); -} - -uint32_t -pneum_get_msg_index (unsigned char * name) -{ - return vl_api_get_msg_index (name); -} diff --git a/src/vpp-api/python/pneum/pneum.h b/src/vpp-api/python/pneum/pneum.h deleted file mode 100644 index c4b55ae0..00000000 --- a/src/vpp-api/python/pneum/pneum.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ -#ifndef included_pneum_h -#define included_pneum_h - -#include -#include - -typedef void (*pneum_callback_t)(unsigned char * data, int len); -int pneum_connect(char * name, char * chroot_prefix, pneum_callback_t cb, - int rx_qlen); -int pneum_disconnect(void); -int pneum_read(char **data, int *l, unsigned short timeout); -int pneum_write(char *data, int len); -void pneum_free(void * msg); -uword * pneum_msg_table_get_hash (void); -int pneum_msg_table_size(void); -uint32_t pneum_get_msg_index(unsigned char * name); -void pneum_rx_suspend (void); -void pneum_rx_resume (void); - -#endif diff --git a/src/vpp-api/python/pneum/test_pneum.c b/src/vpp-api/python/pneum/test_pneum.c deleted file mode 100644 index 334e58e9..00000000 --- a/src/vpp-api/python/pneum/test_pneum.c +++ /dev/null @@ -1,143 +0,0 @@ -/* - *------------------------------------------------------------------ - * test_pneum.c - * - * 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. - *------------------------------------------------------------------ - */ -#include -#include -#include -#include -#include -#include -#include -#include - -#include /* time_t, time (for timestamp in second) */ -#include /* ftime, timeb (for timestamp in millisecond) */ -#include /* gettimeofday, timeval (for timestamp in microsecond) */ - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include "pneum.h" - -#define vl_typedefs /* define message structures */ -#include -#undef vl_typedefs - -/* we are not linking with vlib */ -vlib_main_t vlib_global_main; -vlib_main_t **vlib_mains; - -volatile int sigterm_received = 0; -volatile u32 result_ready; -volatile u16 result_msg_id; - -/* M_NOALLOC: construct, but don't yet send a message */ - -#define M_NOALLOC(T,t) \ - do { \ - result_ready = 0; \ - memset (mp, 0, sizeof (*mp)); \ - mp->_vl_msg_id = ntohs (VL_API_##T); \ - mp->client_index = am->my_client_index; \ - } while(0); - - - -int -wrap_pneum_callback (char *data, int len) -{ - //printf("Callback %d\n", len); - result_ready = 1; - result_msg_id = ntohs(*((u16 *)data)); - return (0); -} - -int main (int argc, char ** argv) -{ - api_main_t * am = &api_main; - vl_api_show_version_t message; - vl_api_show_version_t *mp; - int async = 1; - int rv = pneum_connect("pneum_client", NULL, NULL, 32 /* rx queue-length*/); - - if (rv != 0) { - printf("Connect failed: %d\n", rv); - exit(rv); - } - - struct timeb timer_msec; - long long int timestamp_msec_start; /* timestamp in millisecond. */ - if (!ftime(&timer_msec)) { - timestamp_msec_start = ((long long int) timer_msec.time) * 1000ll + - (long long int) timer_msec.millitm; - } - else { - timestamp_msec_start = -1; - } - - - /* - * Test vpe_api_write and vpe_api_read to send and recv message for an - * API - */ - int i; - long int no_msgs = 10000; - mp = &message; - - for (i = 0; i < no_msgs; i++) { - /* Construct the API message */ - M_NOALLOC(SHOW_VERSION, show_version); - pneum_write((char *)mp, sizeof(*mp)); -#ifndef __COVERITY__ - /* As given, async is always 1. Shut up Coverity about it */ - if (!async) - while (result_ready == 0); -#endif - } - if (async) { - vl_api_control_ping_t control; - vl_api_control_ping_t *mp; - mp = &control; - M_NOALLOC(CONTROL_PING, control_ping); - pneum_write((char *)mp, sizeof(*mp)); - - while (result_msg_id != VL_API_CONTROL_PING_REPLY); - } - - long long int timestamp_msec_end; /* timestamp in millisecond. */ - if (!ftime(&timer_msec)) { - timestamp_msec_end = ((long long int) timer_msec.time) * 1000ll + - (long long int) timer_msec.millitm; - } - else { - timestamp_msec_end = -1; - } - - printf("Took %lld msec, %lld msgs/msec \n", (timestamp_msec_end - timestamp_msec_start), - no_msgs/(timestamp_msec_end - timestamp_msec_start)); - fformat(stdout, "Exiting...\n"); - pneum_disconnect(); - exit (0); -} diff --git a/src/vpp-api/python/setup.cfg b/src/vpp-api/python/setup.cfg index d645be77..79bc6784 100644 --- a/src/vpp-api/python/setup.cfg +++ b/src/vpp-api/python/setup.cfg @@ -2,4 +2,4 @@ # This flag says that the code is written to work on both Python 2 and Python # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. -universal=0 +universal=1 diff --git a/src/vpp-api/python/setup.py b/src/vpp-api/python/setup.py index 8a34d501..28c2ecce 100644 --- a/src/vpp-api/python/setup.py +++ b/src/vpp-api/python/setup.py @@ -13,23 +13,18 @@ # limitations under the License. try: - from setuptools import setup, Extension + from setuptools import setup except ImportError: - from distutils.core import setup, Extension + from distutils.core import setup setup (name = 'vpp_papi', - version = '1.3', + version = '1.4', description = 'VPP Python binding', author = 'Ole Troan', author_email = 'ot@cisco.com', test_suite = 'tests', - packages=['vpp_papi'], - ext_modules = [ - Extension( - 'vpp_api', - sources = ['vpp_papi/pneum_wrap.c'], - libraries = ['pneum'], - )], + install_requires=['cffi'], + py_modules=['vpp_papi'], long_description = '''VPP Python language binding.''', zip_safe = True, ) diff --git a/src/vpp-api/python/vpp_papi.py b/src/vpp-api/python/vpp_papi.py new file mode 100644 index 00000000..81f6903b --- /dev/null +++ b/src/vpp-api/python/vpp_papi.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python +# +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import print_function +import sys, os, logging, collections, struct, json, threading, glob +import atexit, Queue + +from cffi import FFI +ffi = FFI() +ffi.cdef(""" +typedef void (*pneum_callback_t)(unsigned char * data, int len); +typedef void (*pneum_error_callback_t)(void *, unsigned char *, int); +int pneum_connect(char * name, char * chroot_prefix, pneum_callback_t cb, + int rx_qlen); +int pneum_disconnect(void); +int pneum_read(char **data, int *l, unsigned short timeout); +int pneum_write(char *data, int len); +void pneum_free(void * msg); + +int pneum_get_msg_index(unsigned char * name); +int pneum_msg_table_size(void); +int pneum_msg_table_max_index(void); + +void pneum_rx_suspend (void); +void pneum_rx_resume (void); +void pneum_set_error_handler(pneum_error_callback_t); + """) + +# Barfs on failure, no need to check success. +vpp_api = ffi.dlopen('libpneum.so') + +def vpp_atexit(self): + """Clean up VPP connection on shutdown.""" + if self.connected: + self.logger.debug('Cleaning up VPP on exit') + self.disconnect() + +vpp_object = None + +@ffi.callback("void(unsigned char *, int)") +def pneum_callback_sync(data, len): + vpp_object.msg_handler_sync(ffi.buffer(data, len)) +@ffi.callback("void(unsigned char *, int)") +def pneum_callback_async(data, len): + vpp_object.msg_handler_async(ffi.buffer(data, len)) +@ffi.callback("void(void *, unsigned char *, int)") +def pneum_error_handler(arg, msg, msg_len): + vpp_object.logger.warning("PNEUM: %s", ffi.string(msg, msg_len)) + +class Empty(object): + pass + + +class FuncWrapper(object): + def __init__(self, func): + self._func = func + self.__name__ = func.__name__ + + def __call__(self, **kwargs): + return self._func(**kwargs) + + +class VPP(): + """VPP interface. + + This class provides the APIs to VPP. The APIs are loaded + from provided .api.json files and makes functions accordingly. + These functions are documented in the VPP .api files, as they + are dynamically created. + + Additionally, VPP can send callback messages; this class + provides a means to register a callback function to receive + these messages in a background thread. + """ + def __init__(self, apifiles = None, testmode = False, async_thread = True, + logger = logging.getLogger('vpp_papi'), loglevel = 'debug'): + """Create a VPP API object. + + apifiles is a list of files containing API + descriptions that will be loaded - methods will be + dynamically created reflecting these APIs. If not + provided this will load the API files from VPP's + default install location. + """ + global vpp_object + vpp_object = self + self.logger = logger + logging.basicConfig(level=getattr(logging, loglevel.upper())) + + self.messages = {} + self.id_names = [] + self.id_msgdef = [] + self.buffersize = 10000 + self.connected = False + self.header = struct.Struct('>HI') + self.apifiles = [] + self.event_callback = None + self.message_queue = Queue.Queue() + self.read_timeout = 0 + self.vpp_api = vpp_api + if async_thread: + self.event_thread = threading.Thread(target=self.thread_msg_handler) + self.event_thread.daemon = True + self.event_thread.start() + + if not apifiles: + # Pick up API definitions from default directory + apifiles = glob.glob('/usr/share/vpp/api/*.api.json') + + for file in apifiles: + with open(file) as apidef_file: + api = json.load(apidef_file) + for t in api['types']: + self.add_type(t[0], t[1:]) + + for m in api['messages']: + self.add_message(m[0], m[1:]) + self.apifiles = apifiles + + # Basic sanity check + if len(self.messages) == 0 and not testmode: + raise ValueError(1, 'Missing JSON message definitions') + + # Make sure we allow VPP to clean up the message rings. + atexit.register(vpp_atexit, self) + + # Register error handler + vpp_api.pneum_set_error_handler(pneum_error_handler) + + class ContextId(object): + """Thread-safe provider of unique context IDs.""" + def __init__(self): + self.context = 0 + self.lock = threading.Lock() + def __call__(self): + """Get a new unique (or, at least, not recently used) context.""" + with self.lock: + self.context += 1 + return self.context + get_context = ContextId() + + def status(self): + """Debug function: report current VPP API status to stdout.""" + print('Connected') if self.connected else print('Not Connected') + print('Read API definitions from', ', '.join(self.apifiles)) + + def __struct (self, t, n = None, e = -1, vl = None): + """Create a packing structure for a message.""" + base_types = { 'u8' : 'B', + 'u16' : 'H', + 'u32' : 'I', + 'i32' : 'i', + 'u64' : 'Q', + 'f64' : 'd', + } + pack = None + if t in base_types: + pack = base_types[t] + if not vl: + if e > 0 and t == 'u8': + # Fixed byte array + return struct.Struct('>' + str(e) + 's') + if e > 0: + # Fixed array of base type + return [e, struct.Struct('>' + base_types[t])] + elif e == 0: + # Old style variable array + return [-1, struct.Struct('>' + base_types[t])] + else: + # Variable length array + return [vl, struct.Struct('>s')] if t == 'u8' else \ + [vl, struct.Struct('>' + base_types[t])] + + return struct.Struct('>' + base_types[t]) + + if t in self.messages: + ### Return a list in case of array ### + if e > 0 and not vl: + return [e, lambda self, encode, buf, offset, args: ( + self.__struct_type(encode, self.messages[t], buf, offset, + args))] + if vl: + return [vl, lambda self, encode, buf, offset, args: ( + self.__struct_type(encode, self.messages[t], buf, offset, + args))] + elif e == 0: + # Old style VLA + raise NotImplementedError(1, 'No support for compound types ' + t) + return lambda self, encode, buf, offset, args: ( + self.__struct_type(encode, self.messages[t], buf, offset, args) + ) + + raise ValueError(1, 'Invalid message type: ' + t) + + def __struct_type(self, encode, msgdef, buf, offset, kwargs): + """Get a message packer or unpacker.""" + if encode: + return self.__struct_type_encode(msgdef, buf, offset, kwargs) + else: + return self.__struct_type_decode(msgdef, buf, offset) + + def __struct_type_encode(self, msgdef, buf, offset, kwargs): + off = offset + size = 0 + + for k in kwargs: + if k not in msgdef['args']: + raise ValueError(1, 'Invalid field-name in message call ' + k) + + for k,v in msgdef['args'].iteritems(): + off += size + if k in kwargs: + if type(v) is list: + if callable(v[1]): + e = kwargs[v[0]] if v[0] in kwargs else v[0] + size = 0 + for i in range(e): + size += v[1](self, True, buf, off + size, + kwargs[k][i]) + else: + if v[0] in kwargs: + l = kwargs[v[0]] + else: + l = len(kwargs[k]) + if v[1].size == 1: + buf[off:off + l] = bytearray(kwargs[k]) + size = l + else: + size = 0 + for i in kwargs[k]: + v[1].pack_into(buf, off + size, i) + size += v[1].size + else: + if callable(v): + size = v(self, True, buf, off, kwargs[k]) + else: + v.pack_into(buf, off, kwargs[k]) + size = v.size + else: + size = v.size if not type(v) is list else 0 + + return off + size - offset + + + def __getitem__(self, name): + if name in self.messages: + return self.messages[name] + return None + + def encode(self, msgdef, kwargs): + # Make suitably large buffer + buf = bytearray(self.buffersize) + offset = 0 + size = self.__struct_type(True, msgdef, buf, offset, kwargs) + return buf[:offset + size] + + def decode(self, msgdef, buf): + return self.__struct_type(False, msgdef, buf, 0, None)[1] + + def __struct_type_decode(self, msgdef, buf, offset): + res = [] + off = offset + size = 0 + for k,v in msgdef['args'].iteritems(): + off += size + if type(v) is list: + lst = [] + if callable(v[1]): # compound type + size = 0 + if v[0] in msgdef['args']: # vla + e = res[v[2]] + else: # fixed array + e = v[0] + res.append(lst) + for i in range(e): + (s,l) = v[1](self, False, buf, off + size, None) + lst.append(l) + size += s + continue + if v[1].size == 1: + if type(v[0]) is int: + size = len(buf) - off + else: + size = res[v[2]] + res.append(buf[off:off + size]) + else: + e = v[0] if type(v[0]) is int else res[v[2]] + if e == -1: + e = (len(buf) - off) / v[1].size + lst = [] + res.append(lst) + size = 0 + for i in range(e): + lst.append(v[1].unpack_from(buf, off + size)[0]) + size += v[1].size + else: + if callable(v): + (s,l) = v(self, False, buf, off, None) + res.append(l) + size += s + else: + res.append(v.unpack_from(buf, off)[0]) + size = v.size + + return off + size - offset, msgdef['return_tuple']._make(res) + + def ret_tup(self, name): + if name in self.messages and 'return_tuple' in self.messages[name]: + return self.messages[name]['return_tuple'] + return None + + def add_message(self, name, msgdef, typeonly = False): + if name in self.messages: + raise ValueError('Duplicate message name: ' + name) + + args = collections.OrderedDict() + argtypes = collections.OrderedDict() + fields = [] + msg = {} + for i, f in enumerate(msgdef): + if type(f) is dict and 'crc' in f: + msg['crc'] = f['crc'] + continue + field_type = f[0] + field_name = f[1] + if len(f) == 3 and f[2] == 0 and i != len(msgdef) - 2: + raise ValueError('Variable Length Array must be last: ' + name) + args[field_name] = self.__struct(*f) + argtypes[field_name] = field_type + if len(f) == 4: # Find offset to # elements field + args[field_name].append(args.keys().index(f[3]) - i) + fields.append(field_name) + msg['return_tuple'] = collections.namedtuple(name, fields, + rename = True) + self.messages[name] = msg + self.messages[name]['args'] = args + self.messages[name]['argtypes'] = argtypes + self.messages[name]['typeonly'] = typeonly + return self.messages[name] + + def add_type(self, name, typedef): + return self.add_message('vl_api_' + name + '_t', typedef, typeonly=True) + + def make_function(self, name, i, msgdef, multipart, async): + if (async): + f = lambda **kwargs: (self._call_vpp_async(i, msgdef, **kwargs)) + else: + f = lambda **kwargs: (self._call_vpp(i, msgdef, multipart, **kwargs)) + args = self.messages[name]['args'] + argtypes = self.messages[name]['argtypes'] + f.__name__ = str(name) + f.__doc__ = ", ".join(["%s %s" % (argtypes[k], k) for k in args.keys()]) + return f + + @property + def api(self): + if not hasattr(self, "_api"): + raise Exception("Not connected, api definitions not available") + return self._api + + def _register_functions(self, async=False): + self.id_names = [None] * (self.vpp_dictionary_maxid + 1) + self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1) + self._api = Empty() + for name, msgdef in self.messages.iteritems(): + if self.messages[name]['typeonly']: continue + crc = self.messages[name]['crc'] + n = name + '_' + crc[2:] + i = vpp_api.pneum_get_msg_index(bytes(n)) + if i > 0: + self.id_msgdef[i] = msgdef + self.id_names[i] = name + multipart = True if name.find('_dump') > 0 else False + f = self.make_function(name, i, msgdef, multipart, async) + setattr(self._api, name, FuncWrapper(f)) + + # old API stuff starts here - will be removed in 17.07 + if hasattr(self, name): + raise NameError( + 3, "Conflicting name in JSON definition: `%s'" % name) + setattr(self, name, f) + # old API stuff ends here + else: + self.logger.debug('No such message type or failed CRC checksum: %s', n) + + def _write (self, buf): + """Send a binary-packed message to VPP.""" + if not self.connected: + raise IOError(1, 'Not connected') + return vpp_api.pneum_write(str(buf), len(buf)) + + def _read (self): + if not self.connected: + raise IOError(1, 'Not connected') + mem = ffi.new("char **") + size = ffi.new("int *") + rv = vpp_api.pneum_read(mem, size, self.read_timeout) + if rv: + raise IOError(rv, 'pneum_read filed') + msg = bytes(ffi.buffer(mem[0], size[0])) + vpp_api.pneum_free(mem[0]) + return msg + + def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen, async): + rv = vpp_api.pneum_connect(name, chroot_prefix, msg_handler, rx_qlen) + if rv != 0: + raise IOError(2, 'Connect failed') + self.connected = True + + self.vpp_dictionary_maxid = vpp_api.pneum_msg_table_max_index() + self._register_functions(async=async) + + # Initialise control ping + crc = self.messages['control_ping']['crc'] + self.control_ping_index = \ + vpp_api.pneum_get_msg_index( + bytes('control_ping' + '_' + crc[2:])) + self.control_ping_msgdef = self.messages['control_ping'] + + def connect(self, name, chroot_prefix = ffi.NULL, + async = False, rx_qlen = 32): + """Attach to VPP. + + name - the name of the client. + chroot_prefix - if VPP is chroot'ed, the prefix of the jail + async - if true, messages are sent without waiting for a reply + rx_qlen - the length of the VPP message receive queue between + client and server. + """ + msg_handler = pneum_callback_sync if not async \ + else pneum_callback_async + return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen, + async) + + def connect_sync (self, name, chroot_prefix = ffi.NULL, rx_qlen = 32): + """Attach to VPP in synchronous mode. Application must poll for events. + + name - the name of the client. + chroot_prefix - if VPP is chroot'ed, the prefix of the jail + rx_qlen - the length of the VPP message receive queue between + client and server. + """ + + return self.connect_internal(name, ffi.NULL, chroot_prefix, rx_qlen, + async=False) + + def disconnect(self): + """Detach from VPP.""" + rv = vpp_api.pneum_disconnect() + self.connected = False + return rv + + def msg_handler_sync(self, msg): + """Process an incoming message from VPP in sync mode. + + The message may be a reply or it may be an async notification. + """ + r = self.decode_incoming_msg(msg) + if r is None: + return + + # If we have a context, then use the context to find any + # request waiting for a reply + context = 0 + if hasattr(r, 'context') and r.context > 0: + context = r.context + + msgname = type(r).__name__ + + if context == 0: + # No context -> async notification that we feed to the callback + self.message_queue.put_nowait(r) + else: + raise IOError(2, 'RPC reply message received in event handler') + + def decode_incoming_msg(self, msg): + if not msg: + self.logger.warning('vpp_api.read failed') + return + + i, ci = self.header.unpack_from(msg, 0) + if self.id_names[i] == 'rx_thread_exit': + return + + # + # Decode message and returns a tuple. + # + msgdef = self.id_msgdef[i] + if not msgdef: + raise IOError(2, 'Reply message undefined') + + r = self.decode(msgdef, msg) + + return r + + def msg_handler_async(self, msg): + """Process a message from VPP in async mode. + + In async mode, all messages are returned to the callback. + """ + r = self.decode_incoming_msg(msg) + if r is None: + return + + msgname = type(r).__name__ + + if self.event_callback: + self.event_callback(msgname, r) + + def _control_ping(self, context): + """Send a ping command.""" + self._call_vpp_async(self.control_ping_index, + self.control_ping_msgdef, + context=context) + + def _call_vpp(self, i, msgdef, multipart, **kwargs): + """Given a message, send the message and await a reply. + + msgdef - the message packing definition + i - the message type index + multipart - True if the message returns multiple + messages in return. + context - context number - chosen at random if not + supplied. + The remainder of the kwargs are the arguments to the API call. + + The return value is the message or message array containing + the response. It will raise an IOError exception if there was + no response within the timeout window. + """ + + if not 'context' in kwargs: + context = self.get_context() + kwargs['context'] = context + else: + context = kwargs['context'] + kwargs['_vl_msg_id'] = i + b = self.encode(msgdef, kwargs) + + vpp_api.pneum_rx_suspend() + self._write(b) + + if multipart: + # Send a ping after the request - we use its response + # to detect that we have seen all results. + self._control_ping(context) + + # Block until we get a reply. + rl = [] + while (True): + msg = self._read() + if not msg: + print('PNEUM ERROR: OH MY GOD') + raise IOError(2, 'PNEUM read failed') + + r = self.decode_incoming_msg(msg) + msgname = type(r).__name__ + if not context in r or r.context == 0 or context != r.context: + self.message_queue.put_nowait(r) + continue + + if not multipart: + rl = r + break + if msgname == 'control_ping_reply': + break + + rl.append(r) + + vpp_api.pneum_rx_resume() + + return rl + + def _call_vpp_async(self, i, msgdef, **kwargs): + """Given a message, send the message and await a reply. + + msgdef - the message packing definition + i - the message type index + context - context number - chosen at random if not + supplied. + The remainder of the kwargs are the arguments to the API call. + """ + if not 'context' in kwargs: + context = self.get_context() + kwargs['context'] = context + else: + context = kwargs['context'] + kwargs['_vl_msg_id'] = i + b = self.encode(msgdef, kwargs) + + self._write(b) + + def register_event_callback(self, callback): + """Register a callback for async messages. + + This will be called for async notifications in sync mode, + and all messages in async mode. In sync mode, replies to + requests will not come here. + + callback is a fn(msg_type_name, msg_type) that will be + called when a message comes in. While this function is + executing, note that (a) you are in a background thread and + may wish to use threading.Lock to protect your datastructures, + and (b) message processing from VPP will stop (so if you take + a long while about it you may provoke reply timeouts or cause + VPP to fill the RX buffer). Passing None will disable the + callback. + """ + self.event_callback = callback + + def thread_msg_handler(self): + """Python thread calling the user registerd message handler. + + This is to emulate the old style event callback scheme. Modern + clients should provide their own thread to poll the event + queue. + """ + while True: + r = self.message_queue.get() + msgname = type(r).__name__ + if self.event_callback: + self.event_callback(msgname, r) diff --git a/src/vpp-api/python/vpp_papi/__init__.py b/src/vpp-api/python/vpp_papi/__init__.py deleted file mode 100644 index 6688ffb8..00000000 --- a/src/vpp-api/python/vpp_papi/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) -from . vpp_papi import * - diff --git a/src/vpp-api/python/vpp_papi/pneum_wrap.c b/src/vpp-api/python/vpp_papi/pneum_wrap.c deleted file mode 100644 index c5a7eea1..00000000 --- a/src/vpp-api/python/vpp_papi/pneum_wrap.c +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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. - */ - -#include -#include "../pneum/pneum.h" -#include - -static PyObject *pneum_callback = NULL; - -static void -wrap_pneum_callback (unsigned char * data, int len) -{ - PyGILState_STATE gstate; - PyObject *result;//, *arglist; - - gstate = PyGILState_Ensure(); - - /* Time to call the callback */ -#if PY_VERSION_HEX >= 0x03000000 - result = PyObject_CallFunction(pneum_callback, "y#", data, len); -#else - result = PyObject_CallFunction(pneum_callback, "s#", data, len); -#endif - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -static PyObject * -wrap_connect (PyObject *self, PyObject *args, PyObject *kw) -{ - char * name, * chroot_prefix = NULL; - int rx_qlen = 32; /* default rx queue length */ - int rv; - PyObject * temp = NULL; - pneum_callback_t cb = NULL; - - if (!PyArg_ParseTuple(args, "sOzi:wrap_connect", - &name, &temp, &chroot_prefix, &rx_qlen)) - return (NULL); - - if (temp != Py_None) - { - if (!PyCallable_Check(temp)) - { - PyErr_SetString(PyExc_TypeError, "parameter must be callable"); - return NULL; - } - - Py_XINCREF(temp); /* Add a reference to new callback */ - Py_XDECREF(pneum_callback); /* Dispose of previous callback */ - pneum_callback = temp; /* Remember new callback */ - cb = wrap_pneum_callback; - } - Py_BEGIN_ALLOW_THREADS - rv = pneum_connect(name, chroot_prefix, cb, rx_qlen); - Py_END_ALLOW_THREADS - return PyLong_FromLong(rv); -} - -static PyObject * -wrap_disconnect (PyObject *self, PyObject *args) -{ - int rv; - Py_BEGIN_ALLOW_THREADS - rv = pneum_disconnect(); - Py_END_ALLOW_THREADS - return PyLong_FromLong(rv); -} - -static PyObject * -wrap_write (PyObject *self, PyObject *args) -{ - char *data; - int len, rv; - - if (!PyArg_ParseTuple(args, "s#", &data, &len)) - return NULL; - - Py_BEGIN_ALLOW_THREADS - rv = pneum_write(data, len); - Py_END_ALLOW_THREADS - - return PyLong_FromLong(rv); -} - -static PyObject * -wrap_read (PyObject *self, PyObject *args) -{ - char *data; - int len, rv; - unsigned short timeout; - - if (!PyArg_ParseTuple(args, "H", &timeout)) - return (NULL); - Py_BEGIN_ALLOW_THREADS - rv = pneum_read(&data, &len, timeout); - Py_END_ALLOW_THREADS - - if (rv != 0) { Py_RETURN_NONE; } -#if PY_VERSION_HEX >= 0x03000000 - PyObject *ret = Py_BuildValue("y#", data, len); -#else - PyObject *ret = Py_BuildValue("s#", data, len); -#endif - pneum_free(data); - if (!ret) { Py_RETURN_NONE; } - - return ret; -} - -static PyObject * -wrap_msg_table (PyObject *self, PyObject *args) -{ - int i = 0, rv = 0; - hash_pair_t *hp; - uword *h = pneum_msg_table_get_hash(); - PyObject *ret = PyList_New(pneum_msg_table_size()); - if (!ret) goto error; - hash_foreach_pair (hp, h, - ({ - PyObject *item = PyTuple_New(2); - if (!item) goto error; - rv = PyTuple_SetItem(item, 0, PyLong_FromLong((u32)hp->value[0])); - if (rv) goto error; - rv = PyTuple_SetItem(item, 1, PyString_FromString((char *)hp->key)); - if (rv) goto error; - PyList_SetItem(ret, i, item); - i++; - })); - - return ret; - - error: - /* TODO: Raise exception */ - printf("msg_table failed"); - Py_RETURN_NONE; -} - -static PyObject * -wrap_suspend (PyObject *self, PyObject *args) -{ - Py_BEGIN_ALLOW_THREADS - pneum_rx_suspend(); - Py_END_ALLOW_THREADS - Py_RETURN_NONE; -} - -static PyObject * -wrap_resume (PyObject *self, PyObject *args) -{ - Py_BEGIN_ALLOW_THREADS - pneum_rx_resume(); - Py_END_ALLOW_THREADS - Py_RETURN_NONE; -} - -static PyMethodDef vpp_api_Methods[] = { - {"connect", wrap_connect, METH_VARARGS, "Connect to the VPP API."}, - {"disconnect", wrap_disconnect, METH_VARARGS, "Disconnect from the VPP API."}, - {"write", wrap_write, METH_VARARGS, "Write data to the VPP API."}, - {"read", wrap_read, METH_VARARGS, "Read data from the VPP API."}, - {"msg_table", wrap_msg_table, METH_VARARGS, "Get API dictionary."}, - {"suspend", wrap_suspend, METH_VARARGS, "Suspend RX thread."}, - {"resume", wrap_resume, METH_VARARGS, "Resume RX thread."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - -#if PY_VERSION_HEX >= 0x03000000 -PyMODINIT_FUNC -PyInit_vpp_api (void) -#else -void -initvpp_api (void) -#endif -{ -#if PY_VERSION_HEX >= 0x03000000 - static struct PyModuleDef vpp_api_module = { -#if PY_VERSION_HEX >= 0x03020000 - PyModuleDef_HEAD_INIT, -#else - { - PyObject_HEAD_INIT(NULL) - NULL, /* m_init */ - 0, /* m_index */ - NULL, /* m_copy */ - }, -#endif - (char *) "vpp_api", - NULL, - -1, - vpp_api_Methods, - NULL, - NULL, - NULL, - NULL - }; -#endif - - /* Ensure threading is initialised */ - if (!PyEval_ThreadsInitialized()) { - PyEval_InitThreads(); - } - -#if PY_VERSION_HEX >= 0x03000000 - return PyModule_Create(&vpp_api_module); -#else - Py_InitModule((char *) "vpp_api", vpp_api_Methods); - return; -#endif -} diff --git a/src/vpp-api/python/vpp_papi/vpp_papi.py b/src/vpp-api/python/vpp_papi/vpp_papi.py deleted file mode 100644 index 0c40f171..00000000 --- a/src/vpp-api/python/vpp_papi/vpp_papi.py +++ /dev/null @@ -1,599 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2016 Cisco and/or its affiliates. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from __future__ import print_function -import sys, os, logging, collections, struct, json, threading, glob -import atexit, Queue - -logging.basicConfig(level=logging.DEBUG) -import vpp_api - -def eprint(*args, **kwargs): - """Print critical diagnostics to stderr.""" - print(*args, file=sys.stderr, **kwargs) - -def vpp_atexit(self): - """Clean up VPP connection on shutdown.""" - if self.connected: - eprint ('Cleaning up VPP on exit') - self.disconnect() - - -class Empty(object): - pass - - -class FuncWrapper(object): - def __init__(self, func): - self._func = func - self.__name__ = func.__name__ - - def __call__(self, **kwargs): - return self._func(**kwargs) - - -class VPP(): - """VPP interface. - - This class provides the APIs to VPP. The APIs are loaded - from provided .api.json files and makes functions accordingly. - These functions are documented in the VPP .api files, as they - are dynamically created. - - Additionally, VPP can send callback messages; this class - provides a means to register a callback function to receive - these messages in a background thread. - """ - def __init__(self, apifiles = None, testmode = False, async_thread = True): - """Create a VPP API object. - - apifiles is a list of files containing API - descriptions that will be loaded - methods will be - dynamically created reflecting these APIs. If not - provided this will load the API files from VPP's - default install location. - """ - self.messages = {} - self.id_names = [] - self.id_msgdef = [] - self.buffersize = 10000 - self.connected = False - self.header = struct.Struct('>HI') - self.apifiles = [] - self.event_callback = None - self.message_queue = Queue.Queue() - self.read_timeout = 0 - self.vpp_api = vpp_api - if async_thread: - self.event_thread = threading.Thread(target=self.thread_msg_handler) - self.event_thread.daemon = True - self.event_thread.start() - - if not apifiles: - # Pick up API definitions from default directory - apifiles = glob.glob('/usr/share/vpp/api/*.api.json') - - for file in apifiles: - with open(file) as apidef_file: - api = json.load(apidef_file) - for t in api['types']: - self.add_type(t[0], t[1:]) - - for m in api['messages']: - self.add_message(m[0], m[1:]) - self.apifiles = apifiles - - # Basic sanity check - if len(self.messages) == 0 and not testmode: - raise ValueError(1, 'Missing JSON message definitions') - - # Make sure we allow VPP to clean up the message rings. - atexit.register(vpp_atexit, self) - - class ContextId(object): - """Thread-safe provider of unique context IDs.""" - def __init__(self): - self.context = 0 - self.lock = threading.Lock() - def __call__(self): - """Get a new unique (or, at least, not recently used) context.""" - with self.lock: - self.context += 1 - return self.context - get_context = ContextId() - - def status(self): - """Debug function: report current VPP API status to stdout.""" - print('Connected') if self.connected else print('Not Connected') - print('Read API definitions from', ', '.join(self.apifiles)) - - def __struct (self, t, n = None, e = -1, vl = None): - """Create a packing structure for a message.""" - base_types = { 'u8' : 'B', - 'u16' : 'H', - 'u32' : 'I', - 'i32' : 'i', - 'u64' : 'Q', - 'f64' : 'd', - } - pack = None - if t in base_types: - pack = base_types[t] - if not vl: - if e > 0 and t == 'u8': - # Fixed byte array - return struct.Struct('>' + str(e) + 's') - if e > 0: - # Fixed array of base type - return [e, struct.Struct('>' + base_types[t])] - elif e == 0: - # Old style variable array - return [-1, struct.Struct('>' + base_types[t])] - else: - # Variable length array - return [vl, struct.Struct('>s')] if t == 'u8' else \ - [vl, struct.Struct('>' + base_types[t])] - - return struct.Struct('>' + base_types[t]) - - if t in self.messages: - ### Return a list in case of array ### - if e > 0 and not vl: - return [e, lambda self, encode, buf, offset, args: ( - self.__struct_type(encode, self.messages[t], buf, offset, - args))] - if vl: - return [vl, lambda self, encode, buf, offset, args: ( - self.__struct_type(encode, self.messages[t], buf, offset, - args))] - elif e == 0: - # Old style VLA - raise NotImplementedError(1, 'No support for compound types ' + t) - return lambda self, encode, buf, offset, args: ( - self.__struct_type(encode, self.messages[t], buf, offset, args) - ) - - raise ValueError(1, 'Invalid message type: ' + t) - - def __struct_type(self, encode, msgdef, buf, offset, kwargs): - """Get a message packer or unpacker.""" - if encode: - return self.__struct_type_encode(msgdef, buf, offset, kwargs) - else: - return self.__struct_type_decode(msgdef, buf, offset) - - def __struct_type_encode(self, msgdef, buf, offset, kwargs): - off = offset - size = 0 - - for k in kwargs: - if k not in msgdef['args']: - raise ValueError(1, 'Invalid field-name in message call ' + k) - - for k,v in msgdef['args'].iteritems(): - off += size - if k in kwargs: - if type(v) is list: - if callable(v[1]): - e = kwargs[v[0]] if v[0] in kwargs else v[0] - size = 0 - for i in range(e): - size += v[1](self, True, buf, off + size, - kwargs[k][i]) - else: - if v[0] in kwargs: - l = kwargs[v[0]] - else: - l = len(kwargs[k]) - if v[1].size == 1: - buf[off:off + l] = bytearray(kwargs[k]) - size = l - else: - size = 0 - for i in kwargs[k]: - v[1].pack_into(buf, off + size, i) - size += v[1].size - else: - if callable(v): - size = v(self, True, buf, off, kwargs[k]) - else: - v.pack_into(buf, off, kwargs[k]) - size = v.size - else: - size = v.size if not type(v) is list else 0 - - return off + size - offset - - - def __getitem__(self, name): - if name in self.messages: - return self.messages[name] - return None - - def encode(self, msgdef, kwargs): - # Make suitably large buffer - buf = bytearray(self.buffersize) - offset = 0 - size = self.__struct_type(True, msgdef, buf, offset, kwargs) - return buf[:offset + size] - - def decode(self, msgdef, buf): - return self.__struct_type(False, msgdef, buf, 0, None)[1] - - def __struct_type_decode(self, msgdef, buf, offset): - res = [] - off = offset - size = 0 - for k,v in msgdef['args'].iteritems(): - off += size - if type(v) is list: - lst = [] - if callable(v[1]): # compound type - size = 0 - if v[0] in msgdef['args']: # vla - e = res[v[2]] - else: # fixed array - e = v[0] - res.append(lst) - for i in range(e): - (s,l) = v[1](self, False, buf, off + size, None) - lst.append(l) - size += s - continue - if v[1].size == 1: - if type(v[0]) is int: - size = len(buf) - off - else: - size = res[v[2]] - res.append(buf[off:off + size]) - else: - e = v[0] if type(v[0]) is int else res[v[2]] - if e == -1: - e = (len(buf) - off) / v[1].size - lst = [] - res.append(lst) - size = 0 - for i in range(e): - lst.append(v[1].unpack_from(buf, off + size)[0]) - size += v[1].size - else: - if callable(v): - (s,l) = v(self, False, buf, off, None) - res.append(l) - size += s - else: - res.append(v.unpack_from(buf, off)[0]) - size = v.size - - return off + size - offset, msgdef['return_tuple']._make(res) - - def ret_tup(self, name): - if name in self.messages and 'return_tuple' in self.messages[name]: - return self.messages[name]['return_tuple'] - return None - - def add_message(self, name, msgdef): - if name in self.messages: - raise ValueError('Duplicate message name: ' + name) - - args = collections.OrderedDict() - argtypes = collections.OrderedDict() - fields = [] - msg = {} - for i, f in enumerate(msgdef): - if type(f) is dict and 'crc' in f: - msg['crc'] = f['crc'] - continue - field_type = f[0] - field_name = f[1] - if len(f) == 3 and f[2] == 0 and i != len(msgdef) - 2: - raise ValueError('Variable Length Array must be last: ' + name) - args[field_name] = self.__struct(*f) - argtypes[field_name] = field_type - if len(f) == 4: # Find offset to # elements field - args[field_name].append(args.keys().index(f[3]) - i) - fields.append(field_name) - msg['return_tuple'] = collections.namedtuple(name, fields, - rename = True) - self.messages[name] = msg - self.messages[name]['args'] = args - self.messages[name]['argtypes'] = argtypes - return self.messages[name] - - def add_type(self, name, typedef): - return self.add_message('vl_api_' + name + '_t', typedef) - - def make_function(self, name, i, msgdef, multipart, async): - if (async): - f = lambda **kwargs: (self._call_vpp_async(i, msgdef, **kwargs)) - else: - f = lambda **kwargs: (self._call_vpp(i, msgdef, multipart, **kwargs)) - args = self.messages[name]['args'] - argtypes = self.messages[name]['argtypes'] - f.__name__ = str(name) - f.__doc__ = ", ".join(["%s %s" % (argtypes[k], k) for k in args.keys()]) - return f - - @property - def api(self): - if not hasattr(self, "_api"): - raise Exception("Not connected, api definitions not available") - return self._api - - def _register_functions(self, async=False): - self.id_names = [None] * (self.vpp_dictionary_maxid + 1) - self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1) - self._api = Empty() - for name, msgdef in self.messages.iteritems(): - if name in self.vpp_dictionary: - if self.messages[name]['crc'] != self.vpp_dictionary[name]['crc']: - raise ValueError(3, 'Failed CRC checksum ' + name + - ' ' + self.messages[name]['crc'] + - ' ' + self.vpp_dictionary[name]['crc']) - i = self.vpp_dictionary[name]['id'] - self.id_msgdef[i] = msgdef - self.id_names[i] = name - multipart = True if name.find('_dump') > 0 else False - f = self.make_function(name, i, msgdef, multipart, async) - setattr(self._api, name, FuncWrapper(f)) - - # old API stuff starts here - will be removed in 17.07 - if hasattr(self, name): - raise NameError( - 3, "Conflicting name in JSON definition: `%s'" % name) - setattr(self, name, f) - # old API stuff ends here - - def _write (self, buf): - """Send a binary-packed message to VPP.""" - if not self.connected: - raise IOError(1, 'Not connected') - return vpp_api.write(str(buf)) - - def _read (self): - if not self.connected: - raise IOError(1, 'Not connected') - - return vpp_api.read(self.read_timeout) - - def _load_dictionary(self): - self.vpp_dictionary = {} - self.vpp_dictionary_maxid = 0 - d = vpp_api.msg_table() - - if not d: - raise IOError(3, 'Cannot get VPP API dictionary') - for i,n in d: - name, crc = n.rsplit('_', 1) - crc = '0x' + crc - self.vpp_dictionary[name] = { 'id' : i, 'crc' : crc } - self.vpp_dictionary_maxid = max(self.vpp_dictionary_maxid, i) - - def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen, async): - rv = vpp_api.connect(name, msg_handler, chroot_prefix, rx_qlen) - if rv != 0: - raise IOError(2, 'Connect failed') - self.connected = True - - self._load_dictionary() - self._register_functions(async=async) - - # Initialise control ping - self.control_ping_index = self.vpp_dictionary['control_ping']['id'] - self.control_ping_msgdef = self.messages['control_ping'] - - def connect(self, name, chroot_prefix = None, async = False, rx_qlen = 32): - """Attach to VPP. - - name - the name of the client. - chroot_prefix - if VPP is chroot'ed, the prefix of the jail - async - if true, messages are sent without waiting for a reply - rx_qlen - the length of the VPP message receive queue between - client and server. - """ - msg_handler = self.msg_handler_sync if not async \ - else self.msg_handler_async - return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen, - async) - - def connect_sync (self, name, chroot_prefix = None, rx_qlen = 32): - """Attach to VPP in synchronous mode. Application must poll for events. - - name - the name of the client. - chroot_prefix - if VPP is chroot'ed, the prefix of the jail - rx_qlen - the length of the VPP message receive queue between - client and server. - """ - - return self.connect_internal(name, None, chroot_prefix, rx_qlen, - async=False) - - def disconnect(self): - """Detach from VPP.""" - rv = vpp_api.disconnect() - self.connected = False - return rv - - def msg_handler_sync(self, msg): - """Process an incoming message from VPP in sync mode. - - The message may be a reply or it may be an async notification. - """ - r = self.decode_incoming_msg(msg) - if r is None: - return - - # If we have a context, then use the context to find any - # request waiting for a reply - context = 0 - if hasattr(r, 'context') and r.context > 0: - context = r.context - - msgname = type(r).__name__ - - if context == 0: - # No context -> async notification that we feed to the callback - self.message_queue.put_nowait(r) - else: - raise IOError(2, 'RPC reply message received in event handler') - - def decode_incoming_msg(self, msg): - if not msg: - eprint('vpp_api.read failed') - return - - i, ci = self.header.unpack_from(msg, 0) - if self.id_names[i] == 'rx_thread_exit': - return - - # - # Decode message and returns a tuple. - # - msgdef = self.id_msgdef[i] - if not msgdef: - raise IOError(2, 'Reply message undefined') - - r = self.decode(msgdef, msg) - - return r - - def msg_handler_async(self, msg): - """Process a message from VPP in async mode. - - In async mode, all messages are returned to the callback. - """ - r = self.decode_incoming_msg(msg) - if r is None: - return - - msgname = type(r).__name__ - - if self.event_callback: - self.event_callback(msgname, r) - - def _control_ping(self, context): - """Send a ping command.""" - self._call_vpp_async(self.control_ping_index, - self.control_ping_msgdef, - context=context) - - def _call_vpp(self, i, msgdef, multipart, **kwargs): - """Given a message, send the message and await a reply. - - msgdef - the message packing definition - i - the message type index - multipart - True if the message returns multiple - messages in return. - context - context number - chosen at random if not - supplied. - The remainder of the kwargs are the arguments to the API call. - - The return value is the message or message array containing - the response. It will raise an IOError exception if there was - no response within the timeout window. - """ - - if not 'context' in kwargs: - context = self.get_context() - kwargs['context'] = context - else: - context = kwargs['context'] - kwargs['_vl_msg_id'] = i - b = self.encode(msgdef, kwargs) - - vpp_api.suspend() - self._write(b) - - if multipart: - # Send a ping after the request - we use its response - # to detect that we have seen all results. - self._control_ping(context) - - # Block until we get a reply. - rl = [] - while (True): - msg = self._read() - if not msg: - print('PNEUM ERROR: OH MY GOD') - raise IOError(2, 'PNEUM read failed') - - r = self.decode_incoming_msg(msg) - msgname = type(r).__name__ - if not context in r or r.context == 0 or context != r.context: - self.message_queue.put_nowait(r) - continue - - if not multipart: - rl = r - break - if msgname == 'control_ping_reply': - break - - rl.append(r) - - vpp_api.resume() - - return rl - - def _call_vpp_async(self, i, msgdef, **kwargs): - """Given a message, send the message and await a reply. - - msgdef - the message packing definition - i - the message type index - context - context number - chosen at random if not - supplied. - The remainder of the kwargs are the arguments to the API call. - """ - if not 'context' in kwargs: - context = self.get_context() - kwargs['context'] = context - else: - context = kwargs['context'] - kwargs['_vl_msg_id'] = i - b = self.encode(msgdef, kwargs) - - self._write(b) - - def register_event_callback(self, callback): - """Register a callback for async messages. - - This will be called for async notifications in sync mode, - and all messages in async mode. In sync mode, replies to - requests will not come here. - - callback is a fn(msg_type_name, msg_type) that will be - called when a message comes in. While this function is - executing, note that (a) you are in a background thread and - may wish to use threading.Lock to protect your datastructures, - and (b) message processing from VPP will stop (so if you take - a long while about it you may provoke reply timeouts or cause - VPP to fill the RX buffer). Passing None will disable the - callback. - """ - self.event_callback = callback - - def thread_msg_handler(self): - """Python thread calling the user registerd message handler. - - This is to emulate the old style event callback scheme. Modern - clients should provide their own thread to poll the event - queue. - """ - while True: - r = self.message_queue.get() - msgname = type(r).__name__ - if self.event_callback: - self.event_callback(msgname, r) diff --git a/test/Makefile b/test/Makefile index 8594633b..4338e096 100644 --- a/test/Makefile +++ b/test/Makefile @@ -30,7 +30,7 @@ UNITTEST_EXTRA_OPTS="-f" endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 git+https://github.com/klement/py-lispnetworking@setup +PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 cffi 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/vpp_papi_provider.py b/test/vpp_papi_provider.py index 2d683dc2..7f9e2ae1 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -55,7 +55,7 @@ class VppPapiProvider(object): for filename in fnmatch.filter(filenames, '*.api.json'): jsonfiles.append(os.path.join(root, filename)) - self.vpp = VPP(jsonfiles) + self.vpp = VPP(jsonfiles, logger=test_class.logger) self._events = deque() def __enter__(self): -- cgit 1.2.3-korg From 993e0edf4e8078d10ddd5efa8156d62ce8f9d0c5 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 16 Mar 2017 09:14:59 +0100 Subject: make test: support out-of-tree tests env EXTERN_TESTS="/path/to/extra/tests" make test causes to run the default test set and tests collected from test_*.py files under subtree specified in EXTERN_TESTS. Change-Id: I58c5471dd6010730278a5b47d4318737d920bc28 Signed-off-by: Klement Sekera --- Makefile | 5 +++-- test/Makefile | 11 ++++++++--- test/run_tests.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 44547067..36d37bf6 100644 --- a/Makefile +++ b/Makefile @@ -222,6 +222,7 @@ export VPP_PYTHON_PREFIX=$(BR)/python define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-install,) make -C test \ + TEST_DIR=$(WS_ROOT)/test \ VPP_TEST_BUILD_DIR=$(BR)/build-$(2)-native \ VPP_TEST_BIN=$(BR)/install-$(2)-native/vpp/bin/vpp \ VPP_TEST_PLUGIN_PATH=$(BR)/install-$(2)-native/vpp/lib64/vpp_plugins \ @@ -252,10 +253,10 @@ test-wipe: @make -C test wipe test-shell: bootstrap - $(call test,vpp_lite,vpp_lite,shell) + $(call test,vpp,vpp,shell) test-shell-debug: bootstrap - $(call test,vpp_lite,vpp_lite_debug,shell) + $(call test,vpp,vpp_debug,shell) test-doc: @make -C test doc diff --git a/test/Makefile b/test/Makefile index 4338e096..c65eaae6 100644 --- a/test/Makefile +++ b/test/Makefile @@ -23,10 +23,15 @@ verify-no-running-vpp: false; \ fi -UNITTEST_EXTRA_OPTS="" +UNITTEST_EXTRA_OPTS= +UNITTEST_FAILFAST_OPTS= ifeq ($(FAILFAST),1) -UNITTEST_EXTRA_OPTS="-f" +UNITTEST_EXTRA_OPTS=-f +endif + +ifneq ($(EXTERN_TESTS),) +UNITTEST_EXTRA_OPTS=$(UNITTEST_FAILFAST_OPTS) -d $(EXTERN_TESTS) endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv @@ -59,7 +64,7 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover $(UNITTEST_EXTRA_OPTS) -p test_\"*.py\"" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS)" endef .PHONY: sanity diff --git a/test/run_tests.py b/test/run_tests.py index 8f2174b1..39b09368 100644 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -1,12 +1,61 @@ #!/usr/bin/env python +import sys import os import unittest +import argparse +import importlib from framework import VppTestRunner + +def add_from_dir(suite, directory): + do_insert = True + for _f in os.listdir(directory): + f = "%s/%s" % (directory, _f) + if os.path.isdir(f): + add_from_dir(suite, f) + continue + if not os.path.isfile(f): + continue + if do_insert: + sys.path.insert(0, directory) + do_insert = False + if not _f.startswith("test_") or not _f.endswith(".py"): + continue + name = "".join(f.split("/")[-1].split(".")[:-1]) + if name in sys.modules: + raise Exception("Duplicate test module `%s' found!" % name) + module = importlib.import_module(name) + for name, cls in module.__dict__.items(): + if not isinstance(cls, type): + continue + if not issubclass(cls, unittest.TestCase): + continue + if name == "VppTestCase": + continue + for method in dir(cls): + if not callable(getattr(cls, method)): + continue + if method.startswith("test_"): + suite.addTest(cls(method)) + if __name__ == '__main__': try: verbose = int(os.getenv("V", 0)) except: verbose = 0 - unittest.main(testRunner=VppTestRunner, module=None, verbosity=verbose) + + parser = argparse.ArgumentParser(description="VPP unit tests") + parser.add_argument("-f", "--failfast", action='count', + help="fast failure flag") + parser.add_argument("-d", "--dir", action='append', type=str, + help="directory containing test files " + "(may be specified multiple times)") + args = parser.parse_args() + failfast = True if args.failfast == 1 else False + + suite = unittest.TestSuite() + for d in args.dir: + print("Adding tests from directory tree %s" % d) + add_from_dir(suite, d) + VppTestRunner(verbosity=verbose, failfast=failfast).run(suite) -- cgit 1.2.3-korg From 47e275bbe4327aabc641e917f738d72693eb2d81 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 21 Mar 2017 08:21:25 +0100 Subject: make test: support out-of-tree plugins usage: env EXTERN_PLUGINS=/path/to/plugins make test Change-Id: I8eece726dfafeff1cffd921c1e18cd3eb7eb64ed Signed-off-by: Klement Sekera --- test/Makefile | 4 ++++ test/framework.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index c65eaae6..6647d67b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -92,6 +92,8 @@ shell: verify-python-path $(PAPI_INSTALL_DONE) echo VPP_TEST_BIN=$(VPP_TEST_BIN);\ echo VPP_TEST_PLUGIN_PATH=$(VPP_TEST_PLUGIN_PATH);\ echo VPP_TEST_INSTALL_PATH=$(VPP_TEST_INSTALL_PATH);\ + echo EXTERN_TESTS=$(EXTERN_TESTS);\ + echo EXTERN_PLUGINS=$(EXTERN_PLUGINS);\ echo LD_LIBRARY_PATH=$(LD_LIBRARY_PATH);\ echo '***';\ exec - pass as unix { coredump-size } argument to vpp" @echo " e.g. COREDUMP_SIZE=4g" @echo " COREDUMP_SIZE=unlimited" + @echo " EXTERN_TESTS= - path to out-of-tree test_.py files containing test cases" + @echo " EXTERN_PLUGINS=- path to out-of-tree plugins to be loaded by vpp under test" @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" diff --git a/test/framework.py b/test/framework.py index d7a00264..fe6a8a53 100644 --- a/test/framework.py +++ b/test/framework.py @@ -146,6 +146,14 @@ class VppTestCase(unittest.TestCase): cls.set_debug_flags(d) cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') + cls.extern_plugin_path = os.getenv('EXTERN_PLUGINS') + plugin_path = None + if cls.plugin_path is not None: + if cls.extern_plugin_path is not None: + plugin_path = "%s:%s" % ( + cls.plugin_path, cls.extern_plugin_path) + elif cls.extern_plugin_path is not None: + plugin_path = cls.extern_plugin_path debug_cli = "" if cls.step or cls.debug_gdb or cls.debug_gdbserver: debug_cli = "cli-listen localhost:5002" @@ -164,8 +172,8 @@ class VppTestCase(unittest.TestCase): "api-segment", "{", "prefix", cls.shm_prefix, "}", "plugins", "{", "plugin", "dpdk_plugin.so", "{", "disable", "}", "}"] - if cls.plugin_path is not None: - cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path]) + if plugin_path is not None: + cls.vpp_cmdline.extend(["plugin_path", plugin_path]) cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline) @classmethod -- cgit 1.2.3-korg From 3747c75a215f082bc52198a7229e1b1e529d7666 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Mon, 10 Apr 2017 06:30:17 +0200 Subject: make test: automatic "vpp finishes startup" check Add code which checks if vpp doesn't crash/exit immediately after startup to aid debugging stuff like mistyped graph node name or so. Refuse to run tests if the vpp is unable to start, complain loudly and print vpp's stderr at critical log level if this happens to make spotting these problems in jenkins easy. Change-Id: I40d3fbd05c822c0534713bae6bef05ecfb0e0c1d Signed-off-by: Klement Sekera --- test/Makefile | 5 +++++ test/framework.py | 41 ++++++++++++++++++++++++++++------------- test/sanity_run_vpp.py | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 test/sanity_run_vpp.py (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 6647d67b..787dd9d1 100644 --- a/test/Makefile +++ b/test/Makefile @@ -78,6 +78,11 @@ sanity: verify-no-running-vpp echo \"* 2. execute debugger: gdb python -ex 'run sanity_import_vpp_papi.py'\" &&\ echo \"*******************************************************************\" &&\ false)" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python sanity_run_vpp.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Sanity check failed, cannot run vpp\" &&\ + echo \"*******************************************************************\" &&\ + false)" test: verify-python-path $(PAPI_INSTALL_DONE) sanity reset $(call retest-func) diff --git a/test/framework.py b/test/framework.py index ce70af2e..fc263e70 100644 --- a/test/framework.py +++ b/test/framework.py @@ -255,6 +255,7 @@ class VppTestCase(unittest.TestCase): cls.verbose = 0 cls.vpp_dead = False cls.registry = VppObjectRegistry() + cls.vpp_startup_failed = False # 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: @@ -273,7 +274,14 @@ class VppTestCase(unittest.TestCase): hook = PollHook(cls) cls.vapi.register_hook(hook) cls.sleep(0.1, "after vpp startup, before initial poll") - hook.poll_vpp() + try: + hook.poll_vpp() + except: + cls.vpp_startup_failed = True + cls.logger.critical( + "VPP died shortly after startup, check the" + " output to standard error for possible cause") + raise try: cls.vapi.connect() except: @@ -325,27 +333,32 @@ class VppTestCase(unittest.TestCase): cls.vpp.communicate() del cls.vpp + if cls.vpp_startup_failed: + stdout_log = cls.logger.info + stderr_log = cls.logger.critical + else: + stdout_log = cls.logger.info + stderr_log = cls.logger.info + if hasattr(cls, 'vpp_stdout_deque'): - cls.logger.info(single_line_delim) - cls.logger.info('VPP output to stdout while running %s:', - cls.__name__) - cls.logger.info(single_line_delim) + stdout_log(single_line_delim) + stdout_log('VPP output to stdout while running %s:', cls.__name__) + stdout_log(single_line_delim) f = open(cls.tempdir + '/vpp_stdout.txt', 'w') vpp_output = "".join(cls.vpp_stdout_deque) f.write(vpp_output) - cls.logger.info('\n%s', vpp_output) - cls.logger.info(single_line_delim) + stdout_log('\n%s', vpp_output) + stdout_log(single_line_delim) if hasattr(cls, 'vpp_stderr_deque'): - cls.logger.info(single_line_delim) - cls.logger.info('VPP output to stderr while running %s:', - cls.__name__) - cls.logger.info(single_line_delim) + stderr_log(single_line_delim) + stderr_log('VPP output to stderr while running %s:', cls.__name__) + stderr_log(single_line_delim) f = open(cls.tempdir + '/vpp_stderr.txt', 'w') vpp_output = "".join(cls.vpp_stderr_deque) f.write(vpp_output) - cls.logger.info('\n%s', vpp_output) - cls.logger.info(single_line_delim) + stderr_log('\n%s', vpp_output) + stderr_log(single_line_delim) @classmethod def tearDownClass(cls): @@ -925,4 +938,6 @@ class VppTestRunner(unittest.TextTestRunner): filter_func) print("%s out of %s tests match specified filters" % ( filtered.countTestCases(), test.countTestCases())) + if not running_extended_tests(): + print("Not running extended tests (some tests will be skipped)") return super(VppTestRunner, self).run(filtered) diff --git a/test/sanity_run_vpp.py b/test/sanity_run_vpp.py new file mode 100644 index 00000000..527b618f --- /dev/null +++ b/test/sanity_run_vpp.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +from __future__ import print_function +from framework import VppTestCase +from hook import VppDiedError +from sys import exit + + +class SanityTestCase(VppTestCase): + """ Dummy test case used to check if VPP is able to start """ + pass + +if __name__ == '__main__': + rc = 0 + tc = SanityTestCase + try: + tc.setUpClass() + except VppDiedError: + rc = -1 + else: + try: + tc.tearDownClass() + except: + pass + + exit(rc) -- cgit 1.2.3-korg From 937bf30183d39859c4a0717fa1a801cd556cfcfa Mon Sep 17 00:00:00 2001 From: vagrant Date: Fri, 7 Apr 2017 01:48:47 +0000 Subject: make test: python interpreter customization Allow using custom python interpreter via 'PYTHON' env variable. E.g. env PYTHON=pypy make test. Get latest pip automatically to properly function in environments, where old pip is available and a proxy is required. This allows testing pypy in a centos vagrant-provided vm. Change-Id: I67a658fc927303468cc67f0ac192317ca2907625 Signed-off-by: Klement Sekera Signed-off-by: Damjan Marion --- Makefile | 1 + test/Makefile | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 8964329b..133c3d45 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,7 @@ define test VPP_TEST_INSTALL_PATH=$(BR)/install-$(2)-native/ \ LD_LIBRARY_PATH=$(BR)/install-$(2)-native/vpp/lib64/ \ EXTENDED_TESTS=$(EXTENDED_TESTS) \ + PYTHON=$(PYTHON) \ $(3) endef diff --git a/test/Makefile b/test/Makefile index 787dd9d1..da45fb83 100644 --- a/test/Makefile +++ b/test/Makefile @@ -36,17 +36,29 @@ endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup -SCAPY_SOURCE=$(PYTHON_VENV_PATH)/lib/python2.7/site-packages/ -BUILD_COV_DIR = $(BR)/test-cov +SCAPY_SOURCE=$(shell find $(PYTHON_VENV_PATH) -name site-packages) +BUILD_COV_DIR=$(BR)/test-cov +GET_PIP_SCRIPT=$(VPP_PYTHON_PREFIX)/get-pip.py PIP_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/pip-install.done PIP_PATCH_DONE=$(VPP_PYTHON_PREFIX)/pip-patch.done PAPI_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/papi-install.done PAPI_INSTALL_FLAGS=$(PIP_INSTALL_DONE) $(PIP_PATCH_DONE) $(PAPI_INSTALL_DONE) -$(PIP_INSTALL_DONE): - @virtualenv $(PYTHON_VENV_PATH) -p python2.7 +ifeq ($(PYTHON),) +PYTHON_INTERP=python2.7 +else +PYTHON_INTERP=$(PYTHON) +endif + +$(GET_PIP_SCRIPT): + @mkdir -p $(VPP_PYTHON_PREFIX) + @bash -c "cd $(VPP_PYTHON_PREFIX) && curl -O https://bootstrap.pypa.io/get-pip.py" + +$(PIP_INSTALL_DONE): $(GET_PIP_SCRIPT) + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python $(GET_PIP_SCRIPT)" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS)" @touch $@ @@ -114,7 +126,7 @@ wipe: reset @rm -f $(PAPI_INSTALL_FLAGS) doc: verify-python-path $(PIP_PATCH_DONE) - @virtualenv $(PYTHON_VENV_PATH) -p python2.7 + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx sphinx-rtd-theme" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" @@ -139,7 +151,7 @@ wipe-cov: wipe .PHONY: checkstyle checkstyle: verify-python-path - @virtualenv $(PYTHON_VENV_PATH) -p python2.7 + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install pep8" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate &&\ pep8 --show-source -v $(WS_ROOT)/test/*.py ||\ -- cgit 1.2.3-korg From cd6cb986ed35db7e8bd2d702b595f2de065dc80f Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 11 May 2017 06:55:32 +0200 Subject: make test: support coverage for out-of-tree plugins Change-Id: Ibd7828b1e1c699630ff450122d8aa317b1c9da26 Signed-off-by: Klement Sekera --- test/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index da45fb83..d0d7d098 100644 --- a/test/Makefile +++ b/test/Makefile @@ -111,6 +111,7 @@ shell: verify-python-path $(PAPI_INSTALL_DONE) echo VPP_TEST_INSTALL_PATH=$(VPP_TEST_INSTALL_PATH);\ echo EXTERN_TESTS=$(EXTERN_TESTS);\ echo EXTERN_PLUGINS=$(EXTERN_PLUGINS);\ + echo EXTERN_COV_DIR=$(EXTERN_COV_DIR);\ echo LD_LIBRARY_PATH=$(LD_LIBRARY_PATH);\ echo '***';\ exec - path to out-of-tree test_.py files containing test cases" @echo " EXTERN_PLUGINS=- path to out-of-tree plugins to be loaded by vpp under test" + @echo " EXTERN_COV_DIR=- path to out-of-tree prefix, where source, object and .gcda files can be found for coverage report" @echo "" @echo "Creating test documentation" @echo " test-doc - generate documentation for test framework" -- cgit 1.2.3-korg From 26261594a0b821770aa0a646cba8399aa6320c2e Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 1 Jun 2017 05:06:45 +0200 Subject: make test: add suddenly missing dependency (six module) Change-Id: I121eaee6b752a6b3bfe1d5ecf803f67147c77beb Signed-off-by: Klement Sekera --- test/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index d0d7d098..8df96404 100644 --- a/test/Makefile +++ b/test/Makefile @@ -35,7 +35,7 @@ UNITTEST_EXTRA_OPTS=$(UNITTEST_FAILFAST_OPTS) -d $(EXTERN_TESTS) endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup +PYTHON_DEPENDS=six scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup SCAPY_SOURCE=$(shell find $(PYTHON_VENV_PATH) -name site-packages) BUILD_COV_DIR=$(BR)/test-cov -- cgit 1.2.3-korg From 3658adcadcc2e568abc985255a6cddcc4871aa87 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Wed, 7 Jun 2017 08:19:47 +0200 Subject: make test: improve debugability Introduce faulthandler to print stack trace to stderr on python crash. Don't disable automatic garbage collection to decrease the chances of running out of memory. Change-Id: I6927a5f6ea9569735d084d4ed3d258950a400d74 Signed-off-by: Klement Sekera --- test/Makefile | 2 +- test/framework.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 8df96404..14c8cd26 100644 --- a/test/Makefile +++ b/test/Makefile @@ -35,7 +35,7 @@ UNITTEST_EXTRA_OPTS=$(UNITTEST_FAILFAST_OPTS) -d $(EXTERN_TESTS) endif PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv -PYTHON_DEPENDS=six scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup +PYTHON_DEPENDS=faulthandler six scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup SCAPY_SOURCE=$(shell find $(PYTHON_VENV_PATH) -name site-packages) BUILD_COV_DIR=$(BR)/test-cov diff --git a/test/framework.py b/test/framework.py index 91915fca..3c9dd29a 100644 --- a/test/framework.py +++ b/test/framework.py @@ -9,6 +9,7 @@ import unittest import tempfile import time import resource +import faulthandler from collections import deque from threading import Thread, Event from inspect import getdoc @@ -940,7 +941,7 @@ class VppTestRunner(unittest.TextTestRunner): :param test: """ - gc.disable() # disable garbage collection, we'll do that manually + faulthandler.enable() # emit stack trace to stderr if killed by signal 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" % ( -- cgit 1.2.3-korg From 94384e4d3a4143578d5140c386188fd389754ebf Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 11 Jul 2017 07:29:37 +0200 Subject: make test: kill all remaining subprocesses on exit This change introduces a wrapper script which kills all processes in the same process group as itself (with the exception of the script). Using this script to run the unit tests should prevent stale processes left behind in some cases (e.g. when test framework crashes). Change-Id: If3b9201c06b87fa6be095721436893207d09b5e4 Signed-off-by: Klement Sekera --- test/Makefile | 2 +- test/scripts/run_with_cleanup.sh | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100755 test/scripts/run_with_cleanup.sh (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 14c8cd26..7416afc2 100644 --- a/test/Makefile +++ b/test/Makefile @@ -76,7 +76,7 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS)" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && setsid scripts/run_with_cleanup.sh python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS)" endef .PHONY: sanity diff --git a/test/scripts/run_with_cleanup.sh b/test/scripts/run_with_cleanup.sh new file mode 100755 index 00000000..dcaa58b4 --- /dev/null +++ b/test/scripts/run_with_cleanup.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +rv=0 + +atexit() { + group_id=`ps -p $$ -o pgid=` + my_id=$$ + ids=`pgrep -g $group_id -d ' ' | sed "s/\b$my_id\b//g"` + echo "Killing possible remaining process IDs: $ids" + for id in $ids + do + if ps -p $id > /dev/null + then + kill -9 $id + fi + done + exit $rv +} + +trap "atexit" SIGINT SIGTERM + +$* +rv=$? +atexit +exit $rv -- cgit 1.2.3-korg From 909a6a1eb92ceacb983bcff088fb512352929c46 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 8 Aug 2017 04:33:53 +0200 Subject: make test: detect hung tests Run tests in a forked process with a set of pipes to communicate keep-alives and overall result. This allows us to detect when e.g. vpp dies mid-API call causing the test to hang waiting for response (which will never come since vpp died). Support setting a (per test case) TIMEOUT make test option to set timeout, with a default timeout of 120 seconds. Example - fail the test suite if any test-case fails to finish within 300s: make TIMEOUT=300 test Change-Id: I0d04f26a7232968f4bf043adf5d5b508f5018717 Signed-off-by: Klement Sekera --- test/Makefile | 1 + test/debug.py | 23 ++++++++++++++++ test/framework.py | 42 +++++++++++++++++++++++++++-- test/hook.py | 29 +++++--------------- test/run_tests.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++--- test/sanity_run_vpp.py | 10 +++++-- 6 files changed, 148 insertions(+), 29 deletions(-) create mode 100644 test/debug.py (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 7416afc2..d48a6b5a 100644 --- a/test/Makefile +++ b/test/Makefile @@ -184,6 +184,7 @@ help: @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 " TIMEOUT= - fail test suite if any single test takes longer than to finish" @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/debug.py b/test/debug.py new file mode 100644 index 00000000..4516b8c1 --- /dev/null +++ b/test/debug.py @@ -0,0 +1,23 @@ +""" debug utilities """ + +import os +import pexpect + +gdb_path = '/usr/bin/gdb' + + +def spawn_gdb(binary_path, core_path, logger): + if os.path.isfile(gdb_path) and os.access(gdb_path, os.X_OK): + # automatically attach gdb + gdb_cmdline = "%s %s %s" % (gdb_path, binary_path, core_path) + gdb = pexpect.spawn(gdb_cmdline) + gdb.interact() + try: + gdb.terminate(True) + except: + pass + if gdb.isalive(): + raise Exception("GDB refused to die...") + else: + logger.error("Debugger '%s' does not exist or is not an " + "executable.." % gdb_path) diff --git a/test/framework.py b/test/framework.py index fd493db3..58b76bbe 100644 --- a/test/framework.py +++ b/test/framework.py @@ -12,7 +12,7 @@ import resource import faulthandler from collections import deque from threading import Thread, Event -from inspect import getdoc +from inspect import getdoc, isclass from traceback import format_exception from logging import FileHandler, DEBUG, Formatter from scapy.packet import Raw @@ -92,6 +92,39 @@ def running_extended_tests(): return False +class KeepAliveReporter(object): + """ + Singleton object which reports test start to parent process + """ + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + + @property + def pipe(self): + return self._pipe + + @pipe.setter + def pipe(self, pipe): + if hasattr(self, '_pipe'): + raise Exception("Internal error - pipe should only be set once.") + self._pipe = pipe + + def send_keep_alive(self, test): + """ + Write current test tmpdir & desc to keep-alive pipe to signal liveness + """ + if isclass(test): + desc = test.__name__ + else: + desc = test.shortDescription() + if not desc: + desc = str(test) + + self.pipe.send((desc, test.vpp_bin, test.tempdir)) + + class VppTestCase(unittest.TestCase): """This subclass is a base class for VPP test cases that are implemented as classes. It provides methods to create and run test case. @@ -257,6 +290,8 @@ class VppTestCase(unittest.TestCase): cls.vpp_dead = False cls.registry = VppObjectRegistry() cls.vpp_startup_failed = False + cls.reporter = KeepAliveReporter() + cls.reporter.send_keep_alive(cls) # 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: @@ -394,6 +429,7 @@ class VppTestCase(unittest.TestCase): def setUp(self): """ Clear trace before running each test""" + self.reporter.send_keep_alive(self) self.logger.debug("--- setUp() for %s.%s(%s) called ---" % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)) @@ -865,13 +901,15 @@ class VppTestRunner(unittest.TextTestRunner): """Class maintaining the results of the tests""" return VppTestResult - def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1, + def __init__(self, pipe, stream=sys.stderr, descriptions=True, verbosity=1, failfast=False, buffer=False, resultclass=None): # ignore stream setting here, use hard-coded stdout to be in sync # with prints from VppTestCase methods ... super(VppTestRunner, self).__init__(sys.stdout, descriptions, verbosity, failfast, buffer, resultclass) + reporter = KeepAliveReporter() + reporter.pipe = pipe test_option = "TEST" diff --git a/test/hook.py b/test/hook.py index 247704ec..f34e0c5b 100644 --- a/test/hook.py +++ b/test/hook.py @@ -1,8 +1,8 @@ import signal import os -import pexpect import traceback -from log import * +from log import RED, single_line_delim, double_line_delim +from debug import spawn_gdb, gdb_path class Hook(object): @@ -60,31 +60,16 @@ class PollHook(Hook): self.testcase = testcase self.logger = testcase.logger - 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.debug_core: - 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: + if not spawn_gdb(self.testcase.vpp_bin, core_path): self.logger.error( "Debugger '%s' does not exist or is not an executable.." % gdb_path) - - self.logger.critical('core file present, debug with: gdb ' + - self.testcase.vpp_bin + ' ' + core_path) + else: + return + self.logger.critical("Core file present, debug with: gdb %s %s" % + (self.testcase.vpp_bin, core_path)) def poll_vpp(self): """ diff --git a/test/run_tests.py b/test/run_tests.py index 1b9c677d..6d477d88 100644 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -2,10 +2,14 @@ import sys import os +import select import unittest import argparse import importlib +from multiprocessing import Process, Pipe from framework import VppTestRunner +from debug import spawn_gdb +from log import global_logger def add_from_dir(suite, directory): @@ -39,12 +43,38 @@ def add_from_dir(suite, directory): if method.startswith("test_"): suite.addTest(cls(method)) + +def test_runner_wrapper(keep_alive_pipe, result_pipe): + result = not VppTestRunner( + pipe=keep_alive_pipe, + verbosity=verbose, + failfast=failfast).run(suite).wasSuccessful() + result_pipe.send(result) + result_pipe.close() + keep_alive_pipe.close() + + +def handle_core(vpp_binary, core_path): + try: + d = os.getenv("DEBUG") + except: + d = None + if d and d.lower() == "core": + spawn_gdb(vpp_binary, core_path, global_logger) + + if __name__ == '__main__': try: verbose = int(os.getenv("V", 0)) except: verbose = 0 + default_test_timeout = 120 + try: + test_timeout = int(os.getenv("TIMEOUT", default_test_timeout)) + except: + test_timeout = default_test_timeout + parser = argparse.ArgumentParser(description="VPP unit tests") parser.add_argument("-f", "--failfast", action='count', help="fast failure flag") @@ -56,7 +86,43 @@ if __name__ == '__main__': suite = unittest.TestSuite() for d in args.dir: - print("Adding tests from directory tree %s" % d) + global_logger.info("Adding tests from directory tree %s" % d) add_from_dir(suite, d) - sys.exit(not VppTestRunner(verbosity=verbose, - failfast=failfast).run(suite).wasSuccessful()) + keep_alive_parent_end, keep_alive_child_end = Pipe(duplex=False) + result_parent_end, result_child_end = Pipe(duplex=False) + + p = Process(target=test_runner_wrapper, + args=(keep_alive_child_end, + result_child_end)) + p.start() + last_test_temp_dir = None + last_test_vpp_binary = None + last_test = None + result = None + while result is None: + readable = select.select([keep_alive_parent_end.fileno(), + result_parent_end.fileno(), + ], + [], [], test_timeout)[0] + if result_parent_end.fileno() in readable: + result = result_parent_end.recv() + elif keep_alive_parent_end.fileno() in readable: + while keep_alive_parent_end.poll(): + last_test, last_test_vpp_binary, last_test_temp_dir =\ + keep_alive_parent_end.recv() + else: + global_logger.critical("Timeout while waiting for child test " + "runner process (last test running was " + "`%s' in `%s')!" % + (last_test, last_test_temp_dir)) + if last_test_temp_dir and last_test_vpp_binary: + core_path = "%s/core" % last_test_temp_dir + if os.path.isfile(core_path): + global_logger.error("Core-file exists in test temporary " + "directory: %s!" % core_path) + handle_core(last_test_vpp_binary, core_path) + p.terminate() + result = -1 + keep_alive_parent_end.close() + result_parent_end.close() + sys.exit(result) diff --git a/test/sanity_run_vpp.py b/test/sanity_run_vpp.py index 527b618f..156608c1 100644 --- a/test/sanity_run_vpp.py +++ b/test/sanity_run_vpp.py @@ -1,9 +1,10 @@ #!/usr/bin/env python from __future__ import print_function -from framework import VppTestCase -from hook import VppDiedError +from multiprocessing import Pipe from sys import exit +from hook import VppDiedError +from framework import VppTestCase, KeepAliveReporter class SanityTestCase(VppTestCase): @@ -13,6 +14,9 @@ class SanityTestCase(VppTestCase): if __name__ == '__main__': rc = 0 tc = SanityTestCase + x, y = Pipe() + reporter = KeepAliveReporter() + reporter.pipe = y try: tc.setUpClass() except VppDiedError: @@ -22,5 +26,7 @@ if __name__ == '__main__': tc.tearDownClass() except: pass + x.close() + y.close() exit(rc) -- cgit 1.2.3-korg From db4e84cf2f8de0909c3483c8cadb25ac72fb3367 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Fri, 11 Aug 2017 10:06:15 +0200 Subject: make test: properly handle ctrl-c Change-Id: Iab88886ebc1582626813777ea45ce97fc8e36443 Signed-off-by: Klement Sekera --- test/Makefile | 10 ++++++++- test/scripts/run_in_venv_with_cleanup.sh | 38 ++++++++++++++++++++++++++++++++ test/scripts/run_with_cleanup.sh | 25 --------------------- test/scripts/setsid_wrapper.sh | 12 ++++++++++ 4 files changed, 59 insertions(+), 26 deletions(-) create mode 100755 test/scripts/run_in_venv_with_cleanup.sh delete mode 100755 test/scripts/run_with_cleanup.sh create mode 100755 test/scripts/setsid_wrapper.sh (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index d48a6b5a..33779dce 100644 --- a/test/Makefile +++ b/test/Makefile @@ -13,6 +13,14 @@ else VPP_PIDS=$(shell pgrep -d, -x vpp_main) endif +ifeq ($(DEBUG),gdb) +FORCE_FOREGROUND=1 +else ifeq ($(DEBUG),gdbserver) +FORCE_FOREGROUND=1 +else +FORCE_FOREGROUND=0 +endif + verify-no-running-vpp: @if [ "$(VPP_PIDS)" != "" ]; then \ echo; \ @@ -76,7 +84,7 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && setsid scripts/run_with_cleanup.sh python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS)" + @scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) endef .PHONY: sanity diff --git a/test/scripts/run_in_venv_with_cleanup.sh b/test/scripts/run_in_venv_with_cleanup.sh new file mode 100755 index 00000000..541f584a --- /dev/null +++ b/test/scripts/run_in_venv_with_cleanup.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +rv=0 + +atexit() { + group_id=`ps -p $$ -o pgid=` + my_id=$$ + ids=`pgrep -g $group_id -d ' ' | sed "s/\b$my_id\b//g"` + echo "Killing possible remaining process IDs: $ids" + for id in $ids + do + if ps -p $id > /dev/null + then + kill -9 $id + fi + done + exit $rv +} + +trap "atexit;" SIGINT SIGTERM + +FORCE_FOREGROUND=$1 +shift + +source $1 +shift + +if [[ "${FORCE_FOREGROUND}" == "1" ]] +then + $* +else + $* & + wait +fi + +rv=$? +atexit +exit $rv diff --git a/test/scripts/run_with_cleanup.sh b/test/scripts/run_with_cleanup.sh deleted file mode 100755 index dcaa58b4..00000000 --- a/test/scripts/run_with_cleanup.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -rv=0 - -atexit() { - group_id=`ps -p $$ -o pgid=` - my_id=$$ - ids=`pgrep -g $group_id -d ' ' | sed "s/\b$my_id\b//g"` - echo "Killing possible remaining process IDs: $ids" - for id in $ids - do - if ps -p $id > /dev/null - then - kill -9 $id - fi - done - exit $rv -} - -trap "atexit" SIGINT SIGTERM - -$* -rv=$? -atexit -exit $rv diff --git a/test/scripts/setsid_wrapper.sh b/test/scripts/setsid_wrapper.sh new file mode 100755 index 00000000..e18b6ad5 --- /dev/null +++ b/test/scripts/setsid_wrapper.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [[ "$1" == "1" ]] +then + setsid scripts/run_in_venv_with_cleanup.sh $* + pid=$! +else + setsid scripts/run_in_venv_with_cleanup.sh $* & + pid=$! + trap "echo setsid_wrapper.sh: got signal, killing child pid ${pid}; kill ${pid}; sleep .1;" SIGINT SIGTERM + wait +fi -- cgit 1.2.3-korg From f413bef1358e014c9a6cb75bd2ec3e1f351e64ff Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Tue, 15 Aug 2017 07:09:02 +0200 Subject: make test: collect symlinks to failed tests Compress files in temporary directories of failed tests and symlink the directories under /tmp/vpp-failed-unittests location - preparation for jenkins archivation. Automatically cleanup the directory at start of test run. The compression is performed only when environment variable COMPRESS_FAILED_TEST_LOGS is set to one of "yes", "y", "1". This is set in verify target, but left unset by default, so when invoking make test by hand, files won't be compressed. Change-Id: I84c8f1c6aa79aa9c0b753357022b1f195f17a283 Signed-off-by: Klement Sekera --- Makefile | 2 +- test/Makefile | 7 ++++++- test/framework.py | 22 +++++++++++++++++++++- test/scripts/compress_failed.sh | 21 +++++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100755 test/scripts/compress_failed.sh (limited to 'test/Makefile') diff --git a/Makefile b/Makefile index 1cce928a..c1a7cbb5 100644 --- a/Makefile +++ b/Makefile @@ -501,7 +501,7 @@ endif $(call banner,"Building $(PKG) packages") @make pkg-$(PKG) ifeq ($(OS_ID)-$(OS_VERSION_ID),ubuntu-16.04) - @make test + @make COMPRESS_FAILED_TEST_LOGS=yes test endif diff --git a/test/Makefile b/test/Makefile index 33779dce..72b4dac7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,5 +1,7 @@ .PHONY: verify-python-path +VPP_TEST_FAILED_DIR=/tmp/vpp-failed-unittests/ + verify-python-path: ifndef VPP_PYTHON_PREFIX $(error VPP_PYTHON_PREFIX is not set) @@ -84,7 +86,8 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) + @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) + @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/compress_failed.sh endef .PHONY: sanity @@ -129,6 +132,8 @@ shell: verify-python-path $(PAPI_INSTALL_DONE) reset: @rm -f /dev/shm/vpp-unittest-* @rm -rf /tmp/vpp-unittest-* + @rm -rf $(VPP_TEST_FAILED_DIR) + @mkdir $(VPP_TEST_FAILED_DIR) wipe: reset @rm -rf $(PYTHON_VENV_PATH) diff --git a/test/framework.py b/test/framework.py index 89d95cb3..008bda3b 100644 --- a/test/framework.py +++ b/test/framework.py @@ -275,7 +275,7 @@ class VppTestCase(unittest.TestCase): gc.collect() # run garbage collection first cls.logger = getLogger(cls.__name__) cls.tempdir = tempfile.mkdtemp( - prefix='vpp-unittest-' + cls.__name__ + '-') + prefix='vpp-unittest-%s-' % cls.__name__) cls.file_handler = FileHandler("%s/log.txt" % cls.tempdir) cls.file_handler.setFormatter( Formatter(fmt='%(asctime)s,%(msecs)03d %(message)s', @@ -781,6 +781,24 @@ class VppTestResult(unittest.TestResult): unittest.TestResult.addSkip(self, test, reason) self.result_string = colorize("SKIP", YELLOW) + def symlink_failed(self, test): + logger = None + if hasattr(test, 'logger'): + logger = test.logger + if hasattr(test, 'tempdir'): + try: + failed_dir = os.getenv('VPP_TEST_FAILED_DIR') + link_path = '%s/%s-FAILED' % (failed_dir, + test.tempdir.split("/")[-1]) + if logger: + logger.debug("creating a link to the failed test") + logger.debug("os.symlink(%s, %s)" % + (test.tempdir, link_path)) + os.symlink(test.tempdir, link_path) + except Exception as e: + if logger: + logger.error(e) + def addFailure(self, test, err): """ Record a test failed result @@ -800,6 +818,7 @@ class VppTestResult(unittest.TestResult): if hasattr(test, 'tempdir'): self.result_string = colorize("FAIL", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' + self.symlink_failed(test) else: self.result_string = colorize("FAIL", RED) + ' [no temp dir]' @@ -822,6 +841,7 @@ class VppTestResult(unittest.TestResult): if hasattr(test, 'tempdir'): self.result_string = colorize("ERROR", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' + self.symlink_failed(test) else: self.result_string = colorize("ERROR", RED) + ' [no temp dir]' diff --git a/test/scripts/compress_failed.sh b/test/scripts/compress_failed.sh new file mode 100755 index 00000000..6076b3b3 --- /dev/null +++ b/test/scripts/compress_failed.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +if [ "$(ls -A ${VPP_TEST_FAILED_DIR})" ] +then + if [ "${COMPRESS_FAILED_TEST_LOGS}" == "yes" ] + then + echo -n "Compressing files in temporary directories from failed test runs..." + cd ${VPP_TEST_FAILED_DIR} + for d in * + do + cd ${d} + find . ! -path . -print0 | xargs -0 -n1 gzip + cd ${VPP_TEST_FAILED_DIR} + done + echo "done." + else + echo "Not compressing files in temporary directories from failed test runs." + fi +else + echo "No symlinks to failed tests' temporary directories found in ${VPP_TEST_FAILED_DIR}." +fi -- cgit 1.2.3-korg From 8f2a4eafeaa439432107563033728e09665c16d9 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 4 May 2017 06:15:18 +0200 Subject: Add new C API Change-Id: I717ce3cd7c867c155de149ec56623269d26d0ff7 Signed-off-by: Klement Sekera --- .gitignore | 1 + Makefile | 11 +- src/Makefile.am | 2 + src/configure.ac | 2 +- src/vlibmemory/unix_shared_memory_queue.c | 65 ++ src/vlibmemory/unix_shared_memory_queue.h | 11 +- src/vpp-api/vapi/Makefile.am | 63 ++ src/vpp-api/vapi/libvapiclient.map | 41 + src/vpp-api/vapi/vapi.c | 895 ++++++++++++++++++++++ src/vpp-api/vapi/vapi.h | 285 +++++++ src/vpp-api/vapi/vapi_c_gen.py | 809 ++++++++++++++++++++ src/vpp-api/vapi/vapi_dbg.h | 76 ++ src/vpp-api/vapi/vapi_internal.h | 126 ++++ src/vpp-api/vapi/vapi_json_parser.py | 303 ++++++++ test/Makefile | 6 +- test/ext/Makefile | 17 + test/ext/vapi_test.c | 1152 +++++++++++++++++++++++++++++ test/scripts/test-loop.sh | 14 +- test/test_vapi.py | 78 ++ 19 files changed, 3943 insertions(+), 14 deletions(-) create mode 100644 src/vpp-api/vapi/Makefile.am create mode 100644 src/vpp-api/vapi/libvapiclient.map create mode 100644 src/vpp-api/vapi/vapi.c create mode 100644 src/vpp-api/vapi/vapi.h create mode 100755 src/vpp-api/vapi/vapi_c_gen.py create mode 100644 src/vpp-api/vapi/vapi_dbg.h create mode 100644 src/vpp-api/vapi/vapi_internal.h create mode 100644 src/vpp-api/vapi/vapi_json_parser.py create mode 100644 test/ext/Makefile create mode 100644 test/ext/vapi_test.c create mode 100644 test/test_vapi.py (limited to 'test/Makefile') diff --git a/.gitignore b/.gitignore index ba4e104a..5a6266d7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /build-root/test-doc/ /build-root/test-cov/ /build-root/python/ +/build-root/vapi_test/ /build-config.mk /dpdk/*.tar.gz /dpdk/*.tar.xz diff --git a/Makefile b/Makefile index c08115d4..c46fa6bb 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ DEB_DEPENDS = curl build-essential autoconf automake bison libssl-dev ccache DEB_DEPENDS += debhelper dkms git libtool libapr1-dev dh-systemd DEB_DEPENDS += libconfuse-dev git-review exuberant-ctags cscope pkg-config DEB_DEPENDS += lcov chrpath autoconf nasm indent libnuma-dev -DEB_DEPENDS += python-all python-dev python-virtualenv python-pip libffi6 +DEB_DEPENDS += python-all python-dev python-virtualenv python-pip libffi6 check ifeq ($(OS_VERSION_ID),14.04) DEB_DEPENDS += openjdk-8-jdk-headless else ifeq ($(OS_ID)-$(OS_VERSION_ID),debian-8) @@ -76,6 +76,7 @@ RPM_DEPENDS = redhat-lsb glibc-static java-1.8.0-openjdk-devel yum-utils RPM_DEPENDS += apr-devel RPM_DEPENDS += openssl-devel RPM_DEPENDS += numactl-devel +RPM_DEPENDS += check ifeq ($(OS_ID)-$(OS_VERSION_ID),fedora-25) RPM_DEPENDS += python-devel RPM_DEPENDS += python2-virtualenv @@ -101,7 +102,13 @@ endif RPM_SUSE_DEPENDS = autoconf automake bison ccache chrpath distribution-release gcc6 glibc-devel-static RPM_SUSE_DEPENDS += java-1_8_0-openjdk-devel libopenssl-devel libtool lsb-release make openssl-devel -RPM_SUSE_DEPENDS += python-devel python-pip python-rpm-macros shadow nasm libnuma-devel +RPM_SUSE_DEPENDS += python-devel python-pip python-rpm-macros shadow nasm libnuma-devel python3 + +ifeq ($(filter rhel centos,$(OS_ID)),$(OS_ID)) + RPM_DEPENDS += python34 +else + RPM_DEPENDS += python3 +endif ifneq ($(wildcard $(STARTUP_DIR)/startup.conf),) STARTUP_CONF ?= $(STARTUP_DIR)/startup.conf diff --git a/src/Makefile.am b/src/Makefile.am index 41076e0e..7b35e50c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -80,6 +80,8 @@ if ENABLE_JAPI SUBDIRS += vpp-api/java endif +SUBDIRS += vpp-api/vapi + ############################################################################### # API ############################################################################### diff --git a/src/configure.ac b/src/configure.ac index 6b6d9636..2efb23ad 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -3,7 +3,7 @@ LT_INIT AC_CONFIG_AUX_DIR([.]) AM_INIT_AUTOMAKE([subdir-objects]) AM_SILENT_RULES([yes]) -AC_CONFIG_FILES([Makefile plugins/Makefile vpp-api/python/Makefile vpp-api/java/Makefile]) +AC_CONFIG_FILES([Makefile plugins/Makefile vpp-api/python/Makefile vpp-api/java/Makefile vpp-api/vapi/Makefile]) AC_CONFIG_MACRO_DIR([m4]) AC_PROG_CC diff --git a/src/vlibmemory/unix_shared_memory_queue.c b/src/vlibmemory/unix_shared_memory_queue.c index e86edec3..4db4851c 100644 --- a/src/vlibmemory/unix_shared_memory_queue.c +++ b/src/vlibmemory/unix_shared_memory_queue.c @@ -234,6 +234,71 @@ unix_shared_memory_queue_add (unix_shared_memory_queue_t * q, return 0; } +/* + * unix_shared_memory_queue_add2 + */ +int +unix_shared_memory_queue_add2 (unix_shared_memory_queue_t * q, u8 * elem, + u8 * elem2, int nowait) +{ + i8 *tailp; + int need_broadcast = 0; + + if (nowait) + { + /* zero on success */ + if (pthread_mutex_trylock (&q->mutex)) + { + return (-1); + } + } + else + pthread_mutex_lock (&q->mutex); + + if (PREDICT_FALSE (q->cursize + 1 == q->maxsize)) + { + if (nowait) + { + pthread_mutex_unlock (&q->mutex); + return (-2); + } + while (q->cursize + 1 == q->maxsize) + { + (void) pthread_cond_wait (&q->condvar, &q->mutex); + } + } + + tailp = (i8 *) (&q->data[0] + q->elsize * q->tail); + clib_memcpy (tailp, elem, q->elsize); + + q->tail++; + q->cursize++; + + if (q->tail == q->maxsize) + q->tail = 0; + + need_broadcast = (q->cursize == 1); + + tailp = (i8 *) (&q->data[0] + q->elsize * q->tail); + clib_memcpy (tailp, elem2, q->elsize); + + q->tail++; + q->cursize++; + + if (q->tail == q->maxsize) + q->tail = 0; + + if (need_broadcast) + { + (void) pthread_cond_broadcast (&q->condvar); + if (q->signal_when_queue_non_empty) + kill (q->consumer_pid, q->signal_when_queue_non_empty); + } + pthread_mutex_unlock (&q->mutex); + + return 0; +} + /* * unix_shared_memory_queue_sub */ diff --git a/src/vlibmemory/unix_shared_memory_queue.h b/src/vlibmemory/unix_shared_memory_queue.h index 13800065..27de3218 100644 --- a/src/vlibmemory/unix_shared_memory_queue.h +++ b/src/vlibmemory/unix_shared_memory_queue.h @@ -21,7 +21,6 @@ #define included_unix_shared_memory_queue_h #include -#include typedef struct _unix_shared_memory_queue { @@ -43,10 +42,12 @@ unix_shared_memory_queue_t *unix_shared_memory_queue_init (int nels, int signal_when_queue_non_empty); void unix_shared_memory_queue_free (unix_shared_memory_queue_t * q); -int unix_shared_memory_queue_add (unix_shared_memory_queue_t * q, - u8 * elem, int nowait); -int unix_shared_memory_queue_sub (unix_shared_memory_queue_t * q, - u8 * elem, int nowait); +int unix_shared_memory_queue_add (unix_shared_memory_queue_t * q, u8 * elem, + int nowait); +int unix_shared_memory_queue_add2 (unix_shared_memory_queue_t * q, u8 * elem, + u8 * elem2, int nowait); +int unix_shared_memory_queue_sub (unix_shared_memory_queue_t * q, u8 * elem, + int nowait); void unix_shared_memory_queue_lock (unix_shared_memory_queue_t * q); void unix_shared_memory_queue_unlock (unix_shared_memory_queue_t * q); int unix_shared_memory_queue_is_full (unix_shared_memory_queue_t * q); diff --git a/src/vpp-api/vapi/Makefile.am b/src/vpp-api/vapi/Makefile.am new file mode 100644 index 00000000..ce681c38 --- /dev/null +++ b/src/vpp-api/vapi/Makefile.am @@ -0,0 +1,63 @@ +# Copyright (c) 2017 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. + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 +AM_LIBTOOLFLAGS = --quiet + +AM_CFLAGS = -Wall -I${top_srcdir} -I${top_builddir} -I. -I$(top_srcdir)/vpp-api/vapi + +AM_LDFLAGS = -shared -avoid-version -rpath /none -no-undefined + +bin_PROGRAMS = +noinst_LTLIBRARIES = +CLEANDIRS = + +%.api.vapi.h: %.api.json vapi_c_gen.py + @echo " VAPI C GEN $< " $@ ; \ + mkdir -p `dirname $@` ; \ + $(top_srcdir)/vpp-api/vapi/vapi_c_gen.py $< + +%.api.json: + find $(top_builddir) -name '$@' | xargs ln -s + +BUILT_SOURCES = $(shell find $(top_builddir) -name '*.api.json' | xargs -n1 basename) \ + $(patsubst %.api.json,%.api.vapi.h,$(JSON_FILES)) + +vapi.c: $(BUILT_SOURCES) + +JSON_FILES = $(wildcard *.api.json) + + +lib_LTLIBRARIES = libvapiclient.la + +libvapiclient_la_SOURCES = vapi.c + +libvapiclient_la_LIBADD = -lpthread -lm -lrt \ + $(top_builddir)/libvppinfra.la \ + $(top_builddir)/libvlibmemoryclient.la \ + $(top_builddir)/libsvm.la + +libvapiclient_la_LDFLAGS = \ + -Wl,-L$(top_builddir)/.libs,--whole-archive,--no-whole-archive \ + -Wl,--version-script=$(srcdir)/libvapiclient.map,-lrt + +libvapiclient_la_CPPFLAGS = -I. -I$(top_builddir)/vpp-api/vapi + +nobase_include_HEADERS = ${top_srcdir}/vpp-api/client/vppapiclient.h \ + vapi.h \ + vapi_dbg.h \ + vapi_internal.h \ + $(patsubst %.api.json,%.api.vapi.h,$(JSON_FILES)) + +# vi:syntax=automake diff --git a/src/vpp-api/vapi/libvapiclient.map b/src/vpp-api/vapi/libvapiclient.map new file mode 100644 index 00000000..53733002 --- /dev/null +++ b/src/vpp-api/vapi/libvapiclient.map @@ -0,0 +1,41 @@ + +VAPICLIENT_17.07 { + global: + vapi_msg_alloc; + vapi_msg_free; + vapi_ctx_alloc; + vapi_ctx_free; + vapi_is_msg_available; + vapi_connect; + vapi_disconnect; + vapi_get_fd; + vapi_send; + vapi_send2; + vapi_recv; + vapi_wait; + vapi_dispatch_one; + vapi_dispatch; + vapi_set_event_cb; + vapi_clear_event_cb; + vapi_set_generic_event_cb; + vapi_clear_generic_event_cb; + vapi_get_client_index; + vapi_register_msg; + vapi_get_client_index; + vapi_is_nonblocking; + vapi_requests_full; + vapi_gen_req_context; + vapi_producer_lock; + vapi_send_with_control_ping; + vapi_store_request; + vapi_is_nonblocking; + vapi_producer_unlock; + vapi_lookup_vl_msg_id; + vapi_lookup_vapi_msg_id_t; + vapi_msg_is_with_context; + vapi_get_context_offset; + vapi_msg_id_control_ping; + vapi_msg_id_control_ping_reply; + + local: *; +}; diff --git a/src/vpp-api/vapi/vapi.c b/src/vpp-api/vapi/vapi.c new file mode 100644 index 00000000..b9c81a13 --- /dev/null +++ b/src/vpp-api/vapi/vapi.c @@ -0,0 +1,895 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 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. + *------------------------------------------------------------------ + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* we need to use control pings for some stuff and because we're forced to put + * the code in headers, we need a way to be able to grab the ids of these + * messages - so declare them here as extern */ +vapi_msg_id_t vapi_msg_id_control_ping = 0; +vapi_msg_id_t vapi_msg_id_control_ping_reply = 0; + +struct +{ + size_t count; + vapi_message_desc_t **msgs; + size_t max_len_name_with_crc; +} __vapi_metadata; + +typedef struct +{ + u32 context; + vapi_cb_t callback; + void *callback_ctx; + bool is_dump; +} vapi_req_t; + +static const u32 context_counter_mask = (1 << 31); + +typedef struct +{ + vapi_error_e (*cb) (vapi_ctx_t ctx, void *callback_ctx, vapi_msg_id_t id, + void *payload); + void *ctx; +} vapi_generic_cb_with_ctx; + +typedef struct +{ + vapi_error_e (*cb) (vapi_ctx_t ctx, void *callback_ctx, void *payload); + void *ctx; +} vapi_event_cb_with_ctx; + +struct vapi_ctx_s +{ + vapi_mode_e mode; + int requests_size; /* size of the requests array (circular queue) */ + int requests_start; /* index of first request */ + int requests_count; /* number of used slots */ + vapi_req_t *requests; + u32 context_counter; + vapi_generic_cb_with_ctx generic_cb; + vapi_event_cb_with_ctx *event_cbs; + u16 *vapi_msg_id_t_to_vl_msg_id; + u16 vl_msg_id_max; + vapi_msg_id_t *vl_msg_id_to_vapi_msg_t; + bool connected; + pthread_mutex_t requests_mutex; +}; + +u32 +vapi_gen_req_context (vapi_ctx_t ctx) +{ + ++ctx->context_counter; + ctx->context_counter %= context_counter_mask; + return ctx->context_counter | context_counter_mask; +} + +size_t +vapi_get_request_count (vapi_ctx_t ctx) +{ + return ctx->requests_count; +} + +bool +vapi_requests_full (vapi_ctx_t ctx) +{ + return (ctx->requests_count == ctx->requests_size); +} + +static bool +vapi_requests_empty (vapi_ctx_t ctx) +{ + return (0 == ctx->requests_count); +} + +static int +vapi_requests_end (vapi_ctx_t ctx) +{ + return (ctx->requests_start + ctx->requests_count) % ctx->requests_size; +} + +void +vapi_store_request (vapi_ctx_t ctx, u32 context, bool is_dump, + vapi_cb_t callback, void *callback_ctx) +{ + assert (!vapi_requests_full (ctx)); + /* if the mutex is not held, bad things will happen */ + assert (0 != pthread_mutex_trylock (&ctx->requests_mutex)); + const int requests_end = vapi_requests_end (ctx); + vapi_req_t *slot = &ctx->requests[requests_end]; + slot->is_dump = is_dump; + slot->context = context; + slot->callback = callback; + slot->callback_ctx = callback_ctx; + VAPI_DBG ("stored@%d: context:%x (start is @%d)", requests_end, context, + ctx->requests_start); + ++ctx->requests_count; + assert (!vapi_requests_empty (ctx)); +} + +#if VAPI_DEBUG_ALLOC +struct to_be_freed_s; +struct to_be_freed_s +{ + void *v; + struct to_be_freed_s *next; +}; + +static struct to_be_freed_s *to_be_freed = NULL; + +void +vapi_add_to_be_freed (void *v) +{ + struct to_be_freed_s *prev = NULL; + struct to_be_freed_s *tmp; + tmp = to_be_freed; + while (tmp && tmp->v) + { + prev = tmp; + tmp = tmp->next; + } + if (!tmp) + { + if (!prev) + { + tmp = to_be_freed = calloc (1, sizeof (*to_be_freed)); + } + else + { + tmp = prev->next = calloc (1, sizeof (*to_be_freed)); + } + } + VAPI_DBG ("To be freed %p", v); + tmp->v = v; +} + +void +vapi_trace_free (void *v) +{ + struct to_be_freed_s *tmp = to_be_freed; + while (tmp && tmp->v != v) + { + tmp = tmp->next; + } + if (tmp && tmp->v == v) + { + VAPI_DBG ("Freed %p", v); + tmp->v = NULL; + } + else + { + VAPI_ERR ("Trying to free untracked pointer %p", v); + abort (); + } +} + +void +vapi_to_be_freed_validate () +{ + struct to_be_freed_s *tmp = to_be_freed; + while (tmp) + { + if (tmp->v) + { + VAPI_ERR ("Unfreed msg %p!", tmp->v); + } + tmp = tmp->next; + } +} + +#endif + +void * +vapi_msg_alloc (vapi_ctx_t ctx, size_t size) +{ + if (!ctx->connected) + { + return NULL; + } + void *rv = vl_msg_api_alloc_or_null (size); + return rv; +} + +void +vapi_msg_free (vapi_ctx_t ctx, void *msg) +{ + if (!ctx->connected) + { + return; + } +#if VAPI_DEBUG_ALLOC + vapi_trace_free (msg); +#endif + vl_msg_api_free (msg); +} + +vapi_error_e +vapi_ctx_alloc (vapi_ctx_t * result) +{ + vapi_ctx_t ctx = calloc (1, sizeof (struct vapi_ctx_s)); + if (!ctx) + { + return VAPI_ENOMEM; + } + ctx->context_counter = 0; + ctx->vapi_msg_id_t_to_vl_msg_id = + malloc (__vapi_metadata.count * + sizeof (*ctx->vapi_msg_id_t_to_vl_msg_id)); + if (!ctx->vapi_msg_id_t_to_vl_msg_id) + { + goto fail; + } + ctx->event_cbs = calloc (__vapi_metadata.count, sizeof (*ctx->event_cbs)); + if (!ctx->event_cbs) + { + goto fail; + } + pthread_mutex_init (&ctx->requests_mutex, NULL); + *result = ctx; + return VAPI_OK; +fail: + vapi_ctx_free (ctx); + return VAPI_ENOMEM; +} + +void +vapi_ctx_free (vapi_ctx_t ctx) +{ + assert (!ctx->connected); + free (ctx->requests); + free (ctx->vapi_msg_id_t_to_vl_msg_id); + free (ctx->event_cbs); + free (ctx->vl_msg_id_to_vapi_msg_t); + pthread_mutex_destroy (&ctx->requests_mutex); + free (ctx); +} + +bool +vapi_is_msg_available (vapi_ctx_t ctx, vapi_msg_id_t id) +{ + return vapi_lookup_vl_msg_id (ctx, id) != UINT16_MAX; +} + +vapi_error_e +vapi_connect (vapi_ctx_t ctx, const char *name, + const char *chroot_prefix, + int max_outstanding_requests, + int response_queue_size, vapi_mode_e mode) +{ + if (response_queue_size <= 0 || max_outstanding_requests <= 0) + { + return VAPI_EINVAL; + } + ctx->requests_size = max_outstanding_requests; + const size_t size = ctx->requests_size * sizeof (*ctx->requests); + void *tmp = realloc (ctx->requests, size); + if (!tmp) + { + return VAPI_ENOMEM; + } + ctx->requests = tmp; + memset (ctx->requests, 0, size); + ctx->requests_start = ctx->requests_count = 0; + if (chroot_prefix) + { + VAPI_DBG ("set memory root path `%s'", chroot_prefix); + vl_set_memory_root_path ((char *) chroot_prefix); + } + static char api_map[] = "/vpe-api"; + VAPI_DBG ("client api map `%s'", api_map); + if ((vl_client_api_map (api_map)) < 0) + { + return VAPI_EMAP_FAIL; + } + VAPI_DBG ("connect client `%s'", name); + if (vl_client_connect ((char *) name, 0, response_queue_size) < 0) + { + vl_client_api_unmap (); + return VAPI_ECON_FAIL; + } +#if VAPI_DEBUG_CONNECT + VAPI_DBG ("start probing messages"); +#endif + int rv; + int i; + for (i = 0; i < __vapi_metadata.count; ++i) + { + vapi_message_desc_t *m = __vapi_metadata.msgs[i]; + u8 scratch[m->name_with_crc_len + 1]; + memcpy (scratch, m->name_with_crc, m->name_with_crc_len + 1); + u32 id = vl_api_get_msg_index (scratch); + if (~0 != id) + { + if (id > UINT16_MAX) + { + VAPI_ERR ("Returned vl_msg_id `%u' > UINT16MAX `%u'!", id, + UINT16_MAX); + rv = VAPI_EINVAL; + goto fail; + } + if (id > ctx->vl_msg_id_max) + { + vapi_msg_id_t *tmp = realloc (ctx->vl_msg_id_to_vapi_msg_t, + sizeof + (*ctx->vl_msg_id_to_vapi_msg_t) * + (id + 1)); + if (!tmp) + { + rv = VAPI_ENOMEM; + goto fail; + } + ctx->vl_msg_id_to_vapi_msg_t = tmp; + ctx->vl_msg_id_max = id; + } + ctx->vl_msg_id_to_vapi_msg_t[id] = m->id; + ctx->vapi_msg_id_t_to_vl_msg_id[m->id] = id; +#if VAPI_DEBUG_CONNECT + VAPI_DBG ("Message `%s' has vl_msg_id `%u'", m->name_with_crc, + (unsigned) id); +#endif + } + else + { + ctx->vapi_msg_id_t_to_vl_msg_id[m->id] = UINT16_MAX; + VAPI_DBG ("Message `%s' not available", m->name_with_crc); + } + } +#if VAPI_DEBUG_CONNECT + VAPI_DBG ("finished probing messages"); +#endif + if (!vapi_is_msg_available (ctx, vapi_msg_id_control_ping) || + !vapi_is_msg_available (ctx, vapi_msg_id_control_ping_reply)) + { + VAPI_ERR + ("control ping or control ping reply not available, cannot connect"); + rv = VAPI_EINCOMPATIBLE; + goto fail; + } + ctx->mode = mode; + ctx->connected = true; + return VAPI_OK; +fail: + vl_client_disconnect (); + vl_client_api_unmap (); + return rv; +} + +vapi_error_e +vapi_disconnect (vapi_ctx_t ctx) +{ + if (!ctx->connected) + { + return VAPI_EINVAL; + } + vl_client_disconnect (); + vl_client_api_unmap (); +#if VAPI_DEBUG_ALLOC + vapi_to_be_freed_validate (); +#endif + ctx->connected = false; + return VAPI_OK; +} + +vapi_error_e +vapi_get_fd (vapi_ctx_t ctx, int *fd) +{ + return VAPI_ENOTSUP; +} + +vapi_error_e +vapi_send (vapi_ctx_t ctx, void *msg) +{ + vapi_error_e rv = VAPI_OK; + if (!ctx || !msg || !ctx->connected) + { + rv = VAPI_EINVAL; + goto out; + } + int tmp; + unix_shared_memory_queue_t *q = api_main.shmem_hdr->vl_input_queue; +#if VAPI_DEBUG + unsigned msgid = be16toh (*(u16 *) msg); + if (msgid <= ctx->vl_msg_id_max) + { + vapi_msg_id_t id = ctx->vl_msg_id_to_vapi_msg_t[msgid]; + if (id < __vapi_metadata.count) + { + VAPI_DBG ("send msg %u[%s]", msgid, __vapi_metadata.msgs[id]->name); + } + else + { + VAPI_DBG ("send msg %u[UNKNOWN]", msgid); + } + } + else + { + VAPI_DBG ("send msg %u[UNKNOWN]", msgid); + } +#endif + tmp = unix_shared_memory_queue_add (q, (u8 *) & msg, + VAPI_MODE_BLOCKING == + ctx->mode ? 0 : 1); + if (tmp < 0) + { + rv = VAPI_EAGAIN; + } +out: + VAPI_DBG ("vapi_send() rv = %d", rv); + return rv; +} + +vapi_error_e +vapi_send2 (vapi_ctx_t ctx, void *msg1, void *msg2) +{ + vapi_error_e rv = VAPI_OK; + if (!ctx || !msg1 || !msg2 || !ctx->connected) + { + rv = VAPI_EINVAL; + goto out; + } + unix_shared_memory_queue_t *q = api_main.shmem_hdr->vl_input_queue; +#if VAPI_DEBUG + unsigned msgid1 = be16toh (*(u16 *) msg1); + unsigned msgid2 = be16toh (*(u16 *) msg2); + const char *name1 = "UNKNOWN"; + const char *name2 = "UNKNOWN"; + if (msgid1 <= ctx->vl_msg_id_max) + { + vapi_msg_id_t id = ctx->vl_msg_id_to_vapi_msg_t[msgid1]; + if (id < __vapi_metadata.count) + { + name1 = __vapi_metadata.msgs[id]->name; + } + } + if (msgid2 <= ctx->vl_msg_id_max) + { + vapi_msg_id_t id = ctx->vl_msg_id_to_vapi_msg_t[msgid2]; + if (id < __vapi_metadata.count) + { + name2 = __vapi_metadata.msgs[id]->name; + } + } + VAPI_DBG ("send two: %u[%s], %u[%s]", msgid1, name1, msgid2, name2); +#endif + int tmp = unix_shared_memory_queue_add2 (q, (u8 *) & msg1, (u8 *) & msg2, + VAPI_MODE_BLOCKING == + ctx->mode ? 0 : 1); + if (tmp < 0) + { + rv = VAPI_EAGAIN; + } +out: + VAPI_DBG ("vapi_send() rv = %d", rv); + return rv; +} + +vapi_error_e +vapi_recv (vapi_ctx_t ctx, void **msg, size_t * msg_size) +{ + if (!ctx || !ctx->connected || !msg || !msg_size) + { + return VAPI_EINVAL; + } + vapi_error_e rv = VAPI_OK; + api_main_t *am = &api_main; + uword data; + + if (am->our_pid == 0) + { + return VAPI_EINVAL; + } + + unix_shared_memory_queue_t *q = am->vl_input_queue; + VAPI_DBG ("doing shm queue sub"); + int tmp = unix_shared_memory_queue_sub (q, (u8 *) & data, 0); + if (tmp == 0) + { +#if VAPI_DEBUG_ALLOC + vapi_add_to_be_freed ((void *) data); +#endif + msgbuf_t *msgbuf = + (msgbuf_t *) ((u8 *) data - offsetof (msgbuf_t, data)); + if (!msgbuf->data_len) + { + vapi_msg_free (ctx, (u8 *) data); + return VAPI_EAGAIN; + } + *msg = (u8 *) data; + *msg_size = ntohl (msgbuf->data_len); + VAPI_DBG ("recv msg %p", *msg); + } + else + { + rv = VAPI_EAGAIN; + } + return rv; +} + +vapi_error_e +vapi_wait (vapi_ctx_t ctx, vapi_wait_mode_e mode) +{ + /* FIXME */ + return VAPI_ENOTSUP; +} + +static vapi_error_e +vapi_dispatch_response (vapi_ctx_t ctx, vapi_msg_id_t id, + u32 context, void *msg) +{ + int mrv; + if (0 != (mrv = pthread_mutex_lock (&ctx->requests_mutex))) + { + VAPI_DBG ("pthread_mutex_lock() failed, rv=%d:%s", mrv, strerror (mrv)); + return VAPI_MUTEX_FAILURE; + } + int tmp = ctx->requests_start; + const int requests_end = vapi_requests_end (ctx); + while (ctx->requests[tmp].context != context && tmp != requests_end) + { + ++tmp; + if (tmp == ctx->requests_size) + { + tmp = 0; + } + } + VAPI_DBG ("dispatch, search from %d, %s at %d", ctx->requests_start, + ctx->requests[tmp].context == context ? "matched" : "stopped", + tmp); + vapi_error_e rv = VAPI_OK; + if (ctx->requests[tmp].context == context) + { + while (ctx->requests_start != tmp) + { + VAPI_ERR ("No response to req with context=%u", + (unsigned) ctx->requests[tmp].context); + ctx->requests[ctx->requests_start].callback (ctx, + ctx->requests + [ctx-> + requests_start].callback_ctx, + VAPI_ENORESP, true, + NULL); + memset (&ctx->requests[ctx->requests_start], 0, + sizeof (ctx->requests[ctx->requests_start])); + ++ctx->requests_start; + --ctx->requests_count; + if (ctx->requests_start == ctx->requests_size) + { + ctx->requests_start = 0; + } + } + // now ctx->requests_start == tmp + int payload_offset = vapi_get_payload_offset (id); + void *payload = ((u8 *) msg) + payload_offset; + bool is_last = true; + if (ctx->requests[tmp].is_dump) + { + if (vapi_msg_id_control_ping_reply == id) + { + payload = NULL; + } + else + { + is_last = false; + } + } + if (payload_offset != -1) + { + rv = + ctx->requests[tmp].callback (ctx, ctx->requests[tmp].callback_ctx, + VAPI_OK, is_last, payload); + } + else + { + /* this is a message without payload, so bend the callback a little + */ + rv = + ((vapi_error_e (*)(vapi_ctx_t, void *, vapi_error_e, bool)) + ctx->requests[tmp].callback) (ctx, + ctx->requests[tmp].callback_ctx, + VAPI_OK, is_last); + } + if (is_last) + { + memset (&ctx->requests[ctx->requests_start], 0, + sizeof (ctx->requests[ctx->requests_start])); + ++ctx->requests_start; + --ctx->requests_count; + if (ctx->requests_start == ctx->requests_size) + { + ctx->requests_start = 0; + } + } + VAPI_DBG ("after dispatch, req start = %d, end = %d, count = %d", + ctx->requests_start, requests_end, ctx->requests_count); + } + if (0 != (mrv = pthread_mutex_unlock (&ctx->requests_mutex))) + { + VAPI_DBG ("pthread_mutex_unlock() failed, rv=%d:%s", mrv, + strerror (mrv)); + abort (); /* this really shouldn't happen */ + } + return rv; +} + +static vapi_error_e +vapi_dispatch_event (vapi_ctx_t ctx, vapi_msg_id_t id, void *msg) +{ + if (ctx->event_cbs[id].cb) + { + return ctx->event_cbs[id].cb (ctx, ctx->event_cbs[id].ctx, msg); + } + else if (ctx->generic_cb.cb) + { + return ctx->generic_cb.cb (ctx, ctx->generic_cb.ctx, id, msg); + } + else + { + VAPI_DBG + ("No handler/generic handler for msg id %u[%s], message ignored", + (unsigned) id, __vapi_metadata.msgs[id]->name); + } + return VAPI_OK; +} + +static bool +vapi_msg_is_with_context (vapi_msg_id_t id) +{ + assert (id <= __vapi_metadata.count); + return __vapi_metadata.msgs[id]->has_context; +} + +vapi_error_e +vapi_dispatch_one (vapi_ctx_t ctx) +{ + VAPI_DBG ("vapi_dispatch_one()"); + void *msg; + size_t size; + vapi_error_e rv = vapi_recv (ctx, &msg, &size); + if (VAPI_OK != rv) + { + VAPI_DBG ("vapi_recv failed with rv=%d", rv); + return rv; + } + u16 vpp_id = be16toh (*(u16 *) msg); + if (vpp_id > ctx->vl_msg_id_max) + { + VAPI_ERR ("Unknown msg ID received, id `%u', out of range <0,%u>", + (unsigned) vpp_id, (unsigned) ctx->vl_msg_id_max); + vapi_msg_free (ctx, msg); + return VAPI_EINVAL; + } + if (~0 == (unsigned) ctx->vl_msg_id_to_vapi_msg_t[vpp_id]) + { + VAPI_ERR ("Unknown msg ID received, id `%u' marked as not supported", + (unsigned) vpp_id); + vapi_msg_free (ctx, msg); + return VAPI_EINVAL; + } + const vapi_msg_id_t id = ctx->vl_msg_id_to_vapi_msg_t[vpp_id]; + const size_t expect_size = vapi_get_message_size (id); + if (size < expect_size) + { + VAPI_ERR + ("Invalid msg received, unexpected size `%zu' < expected min `%zu'", + size, expect_size); + vapi_msg_free (ctx, msg); + return VAPI_EINVAL; + } + u32 context; + vapi_get_swap_to_host_func (id) (msg); + if (vapi_msg_is_with_context (id)) + { + context = *(u32 *) (((u8 *) msg) + vapi_get_context_offset (id)); + /* is this a message originating from VAPI? */ + VAPI_DBG ("dispatch, context is %x", context); + if (context & context_counter_mask) + { + rv = vapi_dispatch_response (ctx, id, context, msg); + goto done; + } + } + rv = vapi_dispatch_event (ctx, id, msg); + +done: + vapi_msg_free (ctx, msg); + return rv; +} + +vapi_error_e +vapi_dispatch (vapi_ctx_t ctx) +{ + vapi_error_e rv = VAPI_OK; + while (!vapi_requests_empty (ctx)) + { + rv = vapi_dispatch_one (ctx); + if (VAPI_OK != rv) + { + return rv; + } + } + return rv; +} + +void +vapi_set_event_cb (vapi_ctx_t ctx, vapi_msg_id_t id, + vapi_event_cb callback, void *callback_ctx) +{ + vapi_event_cb_with_ctx *c = &ctx->event_cbs[id]; + c->cb = callback; + c->ctx = callback_ctx; +} + +void +vapi_clear_event_cb (vapi_ctx_t ctx, vapi_msg_id_t id) +{ + vapi_set_event_cb (ctx, id, NULL, NULL); +} + +void +vapi_set_generic_event_cb (vapi_ctx_t ctx, vapi_generic_event_cb callback, + void *callback_ctx) +{ + ctx->generic_cb.cb = callback; + ctx->generic_cb.ctx = callback_ctx; +} + +void +vapi_clear_generic_event_cb (vapi_ctx_t ctx) +{ + ctx->generic_cb.cb = NULL; + ctx->generic_cb.ctx = NULL; +} + +u16 +vapi_lookup_vl_msg_id (vapi_ctx_t ctx, vapi_msg_id_t id) +{ + assert (id < __vapi_metadata.count); + return ctx->vapi_msg_id_t_to_vl_msg_id[id]; +} + +int +vapi_get_client_index (vapi_ctx_t ctx) +{ + return api_main.my_client_index; +} + +bool +vapi_is_nonblocking (vapi_ctx_t ctx) +{ + return (VAPI_MODE_NONBLOCKING == ctx->mode); +} + +bool vapi_requests_full (vapi_ctx_t ctx); + +size_t vapi_get_request_count (vapi_ctx_t ctx); + +size_t +vapi_get_max_request_count (vapi_ctx_t ctx) +{ + return ctx->requests_size - 1; +} + +int +vapi_get_payload_offset (vapi_msg_id_t id) +{ + assert (id < __vapi_metadata.count); + return __vapi_metadata.msgs[id]->payload_offset; +} + +void (*vapi_get_swap_to_host_func (vapi_msg_id_t id)) (void *msg) +{ + assert (id < __vapi_metadata.count); + return __vapi_metadata.msgs[id]->swap_to_host; +} + +void (*vapi_get_swap_to_be_func (vapi_msg_id_t id)) (void *msg) +{ + assert (id < __vapi_metadata.count); + return __vapi_metadata.msgs[id]->swap_to_be; +} + +size_t +vapi_get_message_size (vapi_msg_id_t id) +{ + assert (id < __vapi_metadata.count); + return __vapi_metadata.msgs[id]->size; +} + +size_t +vapi_get_context_offset (vapi_msg_id_t id) +{ + assert (id < __vapi_metadata.count); + return __vapi_metadata.msgs[id]->context_offset; +} + +vapi_msg_id_t +vapi_register_msg (vapi_message_desc_t * msg) +{ + int i = 0; + for (i = 0; i < __vapi_metadata.count; ++i) + { + if (!strcmp + (msg->name_with_crc, __vapi_metadata.msgs[i]->name_with_crc)) + { + /* this happens if somebody is linking together several objects while + * using the static inline headers, just fill in the already + * assigned id here so that all the objects are in sync */ + msg->id = __vapi_metadata.msgs[i]->id; + return msg->id; + } + } + vapi_msg_id_t id = __vapi_metadata.count; + ++__vapi_metadata.count; + __vapi_metadata.msgs = + realloc (__vapi_metadata.msgs, + sizeof (*__vapi_metadata.msgs) * __vapi_metadata.count); + __vapi_metadata.msgs[id] = msg; + size_t s = strlen (msg->name_with_crc); + if (s > __vapi_metadata.max_len_name_with_crc) + { + __vapi_metadata.max_len_name_with_crc = s; + } + msg->id = id; + return id; +} + +vapi_error_e +vapi_producer_lock (vapi_ctx_t ctx) +{ + int mrv; + if (0 != (mrv = pthread_mutex_lock (&ctx->requests_mutex))) + { + VAPI_DBG ("pthread_mutex_lock() failed, rv=%d:%s", mrv, strerror (mrv)); + (void) mrv; /* avoid warning if the above debug is not enabled */ + return VAPI_MUTEX_FAILURE; + } + return VAPI_OK; +} + +vapi_error_e +vapi_producer_unlock (vapi_ctx_t ctx) +{ + int mrv; + if (0 != (mrv = pthread_mutex_unlock (&ctx->requests_mutex))) + { + VAPI_DBG ("pthread_mutex_unlock() failed, rv=%d:%s", mrv, + strerror (mrv)); + (void) mrv; /* avoid warning if the above debug is not enabled */ + return VAPI_MUTEX_FAILURE; + } + return VAPI_OK; +} + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/src/vpp-api/vapi/vapi.h b/src/vpp-api/vapi/vapi.h new file mode 100644 index 00000000..1e1d567a --- /dev/null +++ b/src/vpp-api/vapi/vapi.h @@ -0,0 +1,285 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 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 vpp_api_h_included +#define vpp_api_h_included + +#include +#include +#include + +/** + * @file vapi.h + * + * common vpp api C declarations + * + * This file declares the common C API functions. These include connect, + * disconnect and utility functions as well as the low-level vapi_send and + * vapi_recv API. This is only the transport layer. + * + * Message formats and higher-level APIs are generated by running the + * vapi_c_gen.py script (which is run for in-tree APIs as part of the build + * process). It's not recommended to mix the higher and lower level APIs. Due + * to version issues, the higher-level APIs are not part of the shared library. + */ + +typedef enum +{ + VAPI_OK = 0, /**< success */ + VAPI_EINVAL, /**< invalid value encountered */ + VAPI_EAGAIN, /**< operation would block */ + VAPI_ENOTSUP, /**< operation not supported */ + VAPI_ENOMEM, /**< out of memory */ + VAPI_ENORESP, /**< no response to request */ + VAPI_EMAP_FAIL, /**< failure while mapping api */ + VAPI_ECON_FAIL, /**< failure while connecting to vpp */ + VAPI_EINCOMPATIBLE, /**< fundamental incompatibility while connecting to vpp + (control ping/control ping reply mismatch) */ + VAPI_MUTEX_FAILURE, /**< failure manipulating internal mutex(es) */ + VAPI_EUSER, /**< user error used for breaking dispatch, + never used by VAPI */ +} vapi_error_e; + +typedef enum +{ + VAPI_MODE_BLOCKING = 1, /**< operations block until response received */ + VAPI_MODE_NONBLOCKING = 2, /**< operations never block */ +} vapi_mode_e; + +typedef enum +{ + VAPI_WAIT_FOR_READ, /**< wait until a message can be read */ + VAPI_WAIT_FOR_WRITE, /**< wait until a message can be written */ + VAPI_WAIT_FOR_READ_WRITE, /**< wait until a read or write can be done */ +} vapi_wait_mode_e; + +typedef int vapi_msg_id_t; +typedef struct vapi_ctx_s *vapi_ctx_t; + +/** + * @brief allocate vapi message of given size + * + * @note message must be freed by vapi_msg_free if not consumed by vapi_send + * call + * + * @param ctx opaque vapi context + * + * @return pointer to message or NULL if out of memory + */ +void *vapi_msg_alloc (vapi_ctx_t ctx, size_t size); + +/** + * @brief free a vapi message + * + * @note messages received by vapi_recv must be freed when no longer needed + * + * @param ctx opaque vapi context + * @param msg message to be freed + */ +void vapi_msg_free (vapi_ctx_t ctx, void *msg); + +/** + * @brief allocate vapi context + * + * @param[out] pointer to result variable + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_ctx_alloc (vapi_ctx_t * result); + +/** + * @brief free vapi context + */ +void vapi_ctx_free (vapi_ctx_t ctx); + +/** + * @brief check if message identified by it's message id is known by the vpp to + * which the connection is open + */ +bool vapi_is_msg_available (vapi_ctx_t ctx, vapi_msg_id_t type); + +/** + * @brief connect to vpp + * + * @param ctx opaque vapi context, must be allocated using vapi_ctx_alloc first + * @param name application name + * @param chroot_prefix shared memory prefix + * @param max_outstanding_requests max number of outstanding requests queued + * @param response_queue_size size of the response queue + * @param mode mode of operation - blocking or nonblocking + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_connect (vapi_ctx_t ctx, const char *name, + const char *chroot_prefix, + int max_outstanding_requests, + int response_queue_size, vapi_mode_e mode); + +/** + * @brief disconnect from vpp + * + * @param ctx opaque vapi context + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_disconnect (vapi_ctx_t ctx); + +/** + * @brief get event file descriptor + * + * @note this file descriptor becomes readable when messages (from vpp) + * are waiting in queue + * + * @param ctx opaque vapi context + * @param[out] fd pointer to result variable + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_get_fd (vapi_ctx_t ctx, int *fd); + +/** + * @brief low-level api for sending messages to vpp + * + * @note it is not recommended to use this api directly, use generated api + * instead + * + * @param ctx opaque vapi context + * @param msg message to send + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_send (vapi_ctx_t ctx, void *msg); + +/** + * @brief low-level api for atomically sending two messages to vpp - either + * both messages are sent or neither one is + * + * @note it is not recommended to use this api directly, use generated api + * instead + * + * @param ctx opaque vapi context + * @param msg1 first message to send + * @param msg2 second message to send + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_send2 (vapi_ctx_t ctx, void *msg1, void *msg2); + +/** + * @brief low-level api for reading messages from vpp + * + * @note it is not recommended to use this api directly, use generated api + * instead + * + * @param ctx opaque vapi context + * @param[out] msg pointer to result variable containing message + * @param[out] msg_size pointer to result variable containing message size + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_recv (vapi_ctx_t ctx, void **msg, size_t * msg_size); + +/** + * @brief wait for connection to become readable or writable + * + * @param ctx opaque vapi context + * @param mode type of property to wait for - readability, writability or both + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_wait (vapi_ctx_t ctx, vapi_wait_mode_e mode); + +/** + * @brief pick next message sent by vpp and call the appropriate callback + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_dispatch_one (vapi_ctx_t ctx); + +/** + * @brief loop vapi_dispatch_one until responses to all currently outstanding + * requests have been received and their callbacks called + * + * @note the dispatch loop is interrupted if any error is encountered or + * returned from the callback, in which case this error is returned as the + * result of vapi_dispatch. In this case it might be necessary to call dispatch + * again to process the remaining messages. Returning VAPI_EUSER from + * a callback allows the user to break the dispatch loop (and distinguish + * this case in the calling code from other failures). VAPI never returns + * VAPI_EUSER on its own. + * + * @return VAPI_OK on success, other error code on error + */ +vapi_error_e vapi_dispatch (vapi_ctx_t ctx); + +/** generic vapi event callback */ +typedef vapi_error_e (*vapi_event_cb) (vapi_ctx_t ctx, void *callback_ctx, + void *payload); + +/** + * @brief set event callback to call when message with given id is dispatched + * + * @param ctx opaque vapi context + * @param id message id + * @param callback callback + * @param callback_ctx context pointer stored and passed to callback + */ +void vapi_set_event_cb (vapi_ctx_t ctx, vapi_msg_id_t id, + vapi_event_cb callback, void *callback_ctx); + +/** + * @brief clear event callback for given message id + * + * @param ctx opaque vapi context + * @param id message id + */ +void vapi_clear_event_cb (vapi_ctx_t ctx, vapi_msg_id_t id); + +/** generic vapi event callback */ +typedef vapi_error_e (*vapi_generic_event_cb) (vapi_ctx_t ctx, + void *callback_ctx, + vapi_msg_id_t id, void *msg); +/** + * @brief set generic event callback + * + * @note this callback is called by dispatch if no message-type specific + * callback is set (so it's a fallback callback) + * + * @param ctx opaque vapi context + * @param callback callback + * @param callback_ctx context pointer stored and passed to callback + */ +void vapi_set_generic_event_cb (vapi_ctx_t ctx, + vapi_generic_event_cb callback, + void *callback_ctx); + +/** + * @brief clear generic event callback + * + * @param ctx opaque vapi context + */ +void vapi_clear_generic_event_cb (vapi_ctx_t ctx); + +#endif + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/src/vpp-api/vapi/vapi_c_gen.py b/src/vpp-api/vapi/vapi_c_gen.py new file mode 100755 index 00000000..2bc1eef8 --- /dev/null +++ b/src/vpp-api/vapi/vapi_c_gen.py @@ -0,0 +1,809 @@ +#!/usr/bin/env python3 + +import argparse +import os +import sys +import logging +from vapi_json_parser import Field, Struct, Message, JsonParser,\ + SimpleType, StructType + + +class CField(Field): + def __init__( + self, + field_name, + field_type, + array_len=None, + nelem_field=None): + super().__init__(field_name, field_type, array_len, nelem_field) + + def get_c_def(self): + if self.len is not None: + return "%s %s[%d]" % (self.type.get_c_name(), self.name, self.len) + else: + return "%s %s" % (self.type.get_c_name(), self.name) + + def get_swap_to_be_code(self, struct, var): + if self.len is not None: + if self.len > 0: + return "do { int i; for (i = 0; i < %d; ++i) { %s } }"\ + " while(0);" % ( + self.len, + self.type.get_swap_to_be_code(struct, "%s[i]" % var)) + else: + if self.nelem_field.needs_byte_swap(): + nelem_field = "%s(%s%s)" % ( + self.nelem_field.type.get_swap_to_host_func_name(), + struct, self.nelem_field.name) + else: + nelem_field = "%s%s" % (struct, self.nelem_field.name) + return ( + "do { int i; for (i = 0; i < %s; ++i) { %s } }" + " while(0);" % + (nelem_field, self.type.get_swap_to_be_code( + struct, "%s[i]" % var))) + return self.type.get_swap_to_be_code(struct, "%s" % var) + + def get_swap_to_host_code(self, struct, var): + if self.len is not None: + if self.len > 0: + return "do { int i; for (i = 0; i < %d; ++i) { %s } }"\ + " while(0);" % ( + self.len, + self.type.get_swap_to_host_code(struct, "%s[i]" % var)) + else: + # nelem_field already swapped to host here... + return ( + "do { int i; for (i = 0; i < %s%s; ++i) { %s } }" + " while(0);" % + (struct, self.nelem_field.name, + self.type.get_swap_to_host_code( + struct, "%s[i]" % var))) + return self.type.get_swap_to_host_code(struct, "%s" % var) + + def needs_byte_swap(self): + return self.type.needs_byte_swap() + + +class CStruct(Struct): + def __init__(self, name, fields): + super().__init__(name, fields) + + def get_c_def(self): + return "\n".join([ + "typedef struct __attribute__((__packed__)) {", + "%s;" % ";\n".join([" %s" % x.get_c_def() + for x in self.fields]), + "} %s;" % self.get_c_name()]) + + +class CSimpleType (SimpleType): + + swap_to_be_dict = { + 'i16': 'htobe16', 'u16': 'htobe16', + 'i32': 'htobe32', 'u32': 'htobe32', + 'i64': 'htobe64', 'u64': 'htobe64', + } + + swap_to_host_dict = { + 'i16': 'be16toh', 'u16': 'be16toh', + 'i32': 'be32toh', 'u32': 'be32toh', + 'i64': 'be64toh', 'u64': 'be64toh', + } + + def __init__(self, name): + super().__init__(name) + + def get_c_name(self): + return self.name + + def get_swap_to_be_func_name(self): + return self.swap_to_be_dict[self.name] + + def get_swap_to_host_func_name(self): + return self.swap_to_host_dict[self.name] + + def get_swap_to_be_code(self, struct, var): + x = "%s%s" % (struct, var) + return "%s = %s(%s);" % (x, self.get_swap_to_be_func_name(), x) + + def get_swap_to_host_code(self, struct, var): + x = "%s%s" % (struct, var) + return "%s = %s(%s);" % (x, self.get_swap_to_host_func_name(), x) + + def needs_byte_swap(self): + try: + self.get_swap_to_host_func_name() + return True + except: + pass + return False + + +class CStructType (StructType, CStruct): + def __init__(self, definition, typedict, field_class): + super().__init__(definition, typedict, field_class) + + def get_c_name(self): + return "vapi_type_%s" % self.name + + def get_swap_to_be_func_name(self): + return "%s_hton" % self.get_c_name() + + def get_swap_to_host_func_name(self): + return "%s_ntoh" % self.get_c_name() + + def get_swap_to_be_func_decl(self): + return "void %s(%s *msg)" % ( + self.get_swap_to_be_func_name(), self.get_c_name()) + + def get_swap_to_be_func_def(self): + return "%s\n{\n%s\n}" % ( + self.get_swap_to_be_func_decl(), + "\n".join([ + " %s" % p.get_swap_to_be_code("msg->", "%s" % p.name) + for p in self.fields if p.needs_byte_swap()]), + ) + + def get_swap_to_host_func_decl(self): + return "void %s(%s *msg)" % ( + self.get_swap_to_host_func_name(), self.get_c_name()) + + def get_swap_to_host_func_def(self): + return "%s\n{\n%s\n}" % ( + self.get_swap_to_host_func_decl(), + "\n".join([ + " %s" % p.get_swap_to_host_code("msg->", "%s" % p.name) + for p in self.fields if p.needs_byte_swap()]), + ) + + def get_swap_to_be_code(self, struct, var): + return "%s(&%s%s);" % (self.get_swap_to_be_func_name(), struct, var) + + def get_swap_to_host_code(self, struct, var): + return "%s(&%s%s);" % (self.get_swap_to_host_func_name(), struct, var) + + def needs_byte_swap(self): + for f in self.fields: + if f.needs_byte_swap(): + return True + return False + + +class CMessage (Message): + def __init__(self, logger, definition, typedict, + struct_type_class, simple_type_class, field_class): + super().__init__(logger, definition, typedict, struct_type_class, + simple_type_class, field_class) + self.payload_members = [ + " %s" % p.get_c_def() + for p in self.fields + if p.type != self.header + ] + + def has_payload(self): + return len(self.payload_members) > 0 + + def get_msg_id_name(self): + return "vapi_msg_id_%s" % self.name + + def get_c_name(self): + return "vapi_msg_%s" % self.name + + def get_payload_struct_name(self): + return "vapi_payload_%s" % self.name + + def get_alloc_func_vla_field_length_name(self, field): + return "%s_array_size" % field.name + + def get_alloc_func_name(self): + return "vapi_alloc_%s" % self.name + + def get_alloc_func_decl(self): + return "%s* %s(struct vapi_ctx_s *ctx%s)" % ( + self.get_c_name(), + self.get_alloc_func_name(), + "".join([", size_t %s" % + self.get_alloc_func_vla_field_length_name(f) + for f in self.fields + if f.nelem_field is not None])) + + def get_alloc_func_def(self): + extra = [] + if self.header.has_field('client_index'): + extra.append( + " msg->header.client_index = vapi_get_client_index(ctx);") + if self.header.has_field('context'): + extra.append(" msg->header.context = 0;") + return "\n".join([ + "%s" % self.get_alloc_func_decl(), + "{", + " %s *msg = NULL;" % self.get_c_name(), + " const size_t size = sizeof(%s)%s;" % ( + self.get_c_name(), + "".join([ + " + sizeof(msg->payload.%s[0]) * %s" % ( + f.name, + self.get_alloc_func_vla_field_length_name(f)) + for f in self.fields + if f.nelem_field is not None + ])), + " msg = vapi_msg_alloc(ctx, size);", + " if (!msg) {", + " return NULL;", + " }", + ] + extra + [ + " msg->header._vl_msg_id = vapi_lookup_vl_msg_id(ctx, %s);" % + self.get_msg_id_name(), + "\n".join([" msg->payload.%s = %s;" % ( + f.nelem_field.name, + self.get_alloc_func_vla_field_length_name(f)) + for f in self.fields + if f.nelem_field is not None]), + " return msg;", + "}"]) + + def get_calc_msg_size_func_name(self): + return "vapi_calc_%s_msg_size" % self.name + + def get_calc_msg_size_func_decl(self): + return "uword %s(%s *msg)" % ( + self.get_calc_msg_size_func_name(), + self.get_c_name()) + + def get_calc_msg_size_func_def(self): + return "\n".join([ + "%s" % self.get_calc_msg_size_func_decl(), + "{", + " return sizeof(*msg)%s;" % + "".join(["+ msg->payload.%s * sizeof(msg->payload.%s[0])" % ( + f.nelem_field.name, + f.name) + for f in self.fields + if f.nelem_field is not None + ]), + "}", + ]) + + def get_c_def(self): + if self.has_payload(): + return "\n".join([ + "typedef struct __attribute__ ((__packed__)) {", + "%s; " % + ";\n".join(self.payload_members), + "} %s;" % self.get_payload_struct_name(), + "", + "typedef struct __attribute__ ((__packed__)) {", + (" %s %s;" % (self.header.get_c_name(), + self.fields[0].name) + if self.header is not None else ""), + " %s payload;" % self.get_payload_struct_name(), + "} %s;" % self.get_c_name(), ]) + else: + return "\n".join([ + "typedef struct __attribute__ ((__packed__)) {", + (" %s %s;" % (self.header.get_c_name(), + self.fields[0].name) + if self.header is not None else ""), + "} %s;" % self.get_c_name(), ]) + + def get_swap_payload_to_host_func_name(self): + return "%s_payload_ntoh" % self.get_c_name() + + def get_swap_payload_to_be_func_name(self): + return "%s_payload_hton" % self.get_c_name() + + def get_swap_payload_to_host_func_decl(self): + return "void %s(%s *payload)" % ( + self.get_swap_payload_to_host_func_name(), + self.get_payload_struct_name()) + + def get_swap_payload_to_be_func_decl(self): + return "void %s(%s *payload)" % ( + self.get_swap_payload_to_be_func_name(), + self.get_payload_struct_name()) + + def get_swap_payload_to_be_func_def(self): + return "%s\n{\n%s\n}" % ( + self.get_swap_payload_to_be_func_decl(), + "\n".join([ + " %s" % p.get_swap_to_be_code("payload->", "%s" % p.name) + for p in self.fields + if p.needs_byte_swap() and p.type != self.header]), + ) + + def get_swap_payload_to_host_func_def(self): + return "%s\n{\n%s\n}" % ( + self.get_swap_payload_to_host_func_decl(), + "\n".join([ + " %s" % p.get_swap_to_host_code("payload->", "%s" % p.name) + for p in self.fields + if p.needs_byte_swap() and p.type != self.header]), + ) + + def get_swap_to_host_func_name(self): + return "%s_ntoh" % self.get_c_name() + + def get_swap_to_be_func_name(self): + return "%s_hton" % self.get_c_name() + + def get_swap_to_host_func_decl(self): + return "void %s(%s *msg)" % ( + self.get_swap_to_host_func_name(), self.get_c_name()) + + def get_swap_to_be_func_decl(self): + return "void %s(%s *msg)" % ( + self.get_swap_to_be_func_name(), self.get_c_name()) + + def get_swap_to_be_func_def(self): + return "\n".join([ + "%s" % self.get_swap_to_be_func_decl(), + "{", + (" VAPI_DBG(\"Swapping `%s'@%%p to big endian\", msg);" % + self.get_c_name()), + " %s(&msg->header);" % self.header.get_swap_to_be_func_name() + if self.header is not None else "", + " %s(&msg->payload);" % self.get_swap_payload_to_be_func_name() + if self.has_payload() else "", + "}", + ]) + + def get_swap_to_host_func_def(self): + return "\n".join([ + "%s" % self.get_swap_to_host_func_decl(), + "{", + (" VAPI_DBG(\"Swapping `%s'@%%p to host byte order\", msg);" % + self.get_c_name()), + " %s(&msg->header);" % self.header.get_swap_to_host_func_name() + if self.header is not None else "", + " %s(&msg->payload);" % self.get_swap_payload_to_host_func_name() + if self.has_payload() else "", + "}", + ]) + + def get_op_func_name(self): + return "vapi_%s" % self.name + + def get_op_func_decl(self): + if self.reply.has_payload(): + return "vapi_error_e %s(%s)" % ( + self.get_op_func_name(), + ",\n ".join([ + 'struct vapi_ctx_s *ctx', + '%s *msg' % self.get_c_name(), + 'vapi_error_e (*callback)(struct vapi_ctx_s *ctx', + ' void *callback_ctx', + ' vapi_error_e rv', + ' bool is_last', + ' %s *reply)' % + self.reply.get_payload_struct_name(), + 'void *callback_ctx', + ]) + ) + else: + return "vapi_error_e %s(%s)" % ( + self.get_op_func_name(), + ",\n ".join([ + 'struct vapi_ctx_s *ctx', + '%s *msg' % self.get_c_name(), + 'vapi_error_e (*callback)(struct vapi_ctx_s *ctx', + ' void *callback_ctx', + ' vapi_error_e rv', + ' bool is_last)', + 'void *callback_ctx', + ]) + ) + + def get_op_func_def(self): + return "\n".join([ + "%s" % self.get_op_func_decl(), + "{", + " if (!msg || !callback) {", + " return VAPI_EINVAL;", + " }", + " if (vapi_is_nonblocking(ctx) && vapi_requests_full(ctx)) {", + " return VAPI_EAGAIN;", + " }", + " vapi_error_e rv;", + " if (VAPI_OK != (rv = vapi_producer_lock (ctx))) {", + " return rv;", + " }", + " u32 req_context = vapi_gen_req_context(ctx);", + " msg->header.context = req_context;", + " %s(msg);" % self.get_swap_to_be_func_name(), + (" if (VAPI_OK == (rv = vapi_send_with_control_ping " + "(ctx, msg, req_context))) {" + if self.is_dump() else + " if (VAPI_OK == (rv = vapi_send (ctx, msg))) {" + ), + (" vapi_store_request(ctx, req_context, %s, " + "(vapi_cb_t)callback, callback_ctx);" % + ("true" if self.is_dump() else "false")), + " if (VAPI_OK != vapi_producer_unlock (ctx)) {", + " abort (); /* this really shouldn't happen */", + " }", + " if (vapi_is_nonblocking(ctx)) {", + " rv = VAPI_OK;", + " } else {", + " rv = vapi_dispatch(ctx);", + " }", + " } else {", + " %s(msg);" % self.get_swap_to_host_func_name(), + " if (VAPI_OK != vapi_producer_unlock (ctx)) {", + " abort (); /* this really shouldn't happen */", + " }", + " }", + " return rv;", + "}", + "", + ]) + + def get_event_cb_func_decl(self): + if not self.is_reply(): + raise Exception( + "Cannot register event callback for non-reply function") + if self.has_payload(): + return "\n".join([ + "void vapi_set_%s_event_cb (" % + self.get_c_name(), + " struct vapi_ctx_s *ctx, ", + (" vapi_error_e (*callback)(struct vapi_ctx_s *ctx, " + "void *callback_ctx, %s *payload)," % + self.get_payload_struct_name()), + " void *callback_ctx)", + ]) + else: + return "\n".join([ + "void vapi_set_%s_event_cb (" % + self.get_c_name(), + " struct vapi_ctx_s *ctx, ", + " vapi_error_e (*callback)(struct vapi_ctx_s *ctx, " + "void *callback_ctx),", + " void *callback_ctx)", + ]) + + def get_event_cb_func_def(self): + if not self.is_reply(): + raise Exception( + "Cannot register event callback for non-reply function") + return "\n".join([ + "%s" % self.get_event_cb_func_decl(), + "{", + (" vapi_set_event_cb(ctx, %s, (vapi_event_cb)callback, " + "callback_ctx);" % + self.get_msg_id_name()), + "}"]) + + def get_c_metadata_struct_name(self): + return "__vapi_metadata_%s" % self.name + + def get_c_constructor(self): + has_context = False + if self.header is not None: + has_context = self.header.has_field('context') + return '\n'.join([ + 'static void __attribute__((constructor)) __vapi_constructor_%s()' + % self.name, + '{', + ' static const char name[] = "%s";' % self.name, + ' static const char name_with_crc[] = "%s_%s";' + % (self.name, self.crc[2:]), + ' static vapi_message_desc_t %s = {' % + self.get_c_metadata_struct_name(), + ' name,', + ' sizeof(name) - 1,', + ' name_with_crc,', + ' sizeof(name_with_crc) - 1,', + ' true,' if has_context else ' false,', + ' offsetof(%s, context),' % self.header.get_c_name() + if has_context else ' 0,', + (' offsetof(%s, payload),' % self.get_c_name()) + if self.has_payload() else '-1,', + ' sizeof(%s),' % self.get_c_name(), + ' (generic_swap_fn_t)%s,' % self.get_swap_to_be_func_name(), + ' (generic_swap_fn_t)%s,' % self.get_swap_to_host_func_name(), + ' ~0,', + ' };', + '', + ' %s = vapi_register_msg(&%s);' % + (self.get_msg_id_name(), self.get_c_metadata_struct_name()), + ' VAPI_DBG("Assigned msg id %%d to %s", %s);' % + (self.name, self.get_msg_id_name()), + '}', + ]) + + +vapi_send_with_control_ping = """ +static inline vapi_error_e +vapi_send_with_control_ping (vapi_ctx_t ctx, void *msg, u32 context) +{ + vapi_msg_control_ping *ping = vapi_alloc_control_ping (ctx); + if (!ping) + { + return VAPI_ENOMEM; + } + ping->header.context = context; + vapi_msg_control_ping_hton (ping); + return vapi_send2 (ctx, msg, ping); +} +""" + + +def gen_json_header(parser, logger, j, io): + logger.info("Generating header `%s'" % io.name) + orig_stdout = sys.stdout + sys.stdout = io + include_guard = "__included_%s" % ( + j.replace(".", "_").replace("/", "_").replace("-", "_")) + print("#ifndef %s" % include_guard) + print("#define %s" % include_guard) + print("") + print("#include ") + print("") + if io.name == "vpe.api.vapi.h": + print("static inline vapi_error_e vapi_send_with_control_ping " + "(vapi_ctx_t ctx, void * msg, u32 context);") + print("") + for m in parser.messages_by_json[j].values(): + print("extern vapi_msg_id_t %s;" % m.get_msg_id_name()) + print("") + for t in parser.types_by_json[j].values(): + try: + print("%s" % t.get_c_def()) + print("") + except: + pass + for t in parser.types_by_json[j].values(): + print("%s;" % t.get_swap_to_be_func_decl()) + print("") + print("%s;" % t.get_swap_to_host_func_decl()) + print("") + for m in parser.messages_by_json[j].values(): + print("%s" % m.get_c_def()) + print("") + for m in parser.messages_by_json[j].values(): + if not m.is_reply(): + print("%s;" % m.get_alloc_func_decl()) + print("") + print("%s;" % m.get_op_func_decl()) + if m.has_payload(): + print("%s;" % m.get_swap_payload_to_be_func_decl()) + print("") + print("%s;" % m.get_swap_payload_to_host_func_decl()) + print("") + print("%s;" % m.get_calc_msg_size_func_decl()) + print("") + print("%s;" % m.get_swap_to_host_func_decl()) + print("") + print("%s;" % m.get_swap_to_be_func_decl()) + print("") + for m in parser.messages_by_json[j].values(): + if not m.is_reply(): + continue + print("%s;" % m.get_event_cb_func_decl()) + print("") + + if io.name == "vpe.api.vapi.h": + print("%s" % vapi_send_with_control_ping) + print("") + + print("#endif") + sys.stdout = orig_stdout + + +def gen_json_code(parser, logger, j, io): + logger.info("Generating code `%s'" % io.name) + orig_stdout = sys.stdout + sys.stdout = io + print("#include <%s>" % json_to_header_name(j)) + print("#include ") + print("#include ") + print("#include ") + print("#include ") + print("#include ") + print("") + for t in parser.types_by_json[j].values(): + print("%s" % t.get_swap_to_be_func_def()) + print("") + print("%s" % t.get_swap_to_host_func_def()) + print("") + for m in parser.messages_by_json[j].values(): + if m.has_payload(): + print("%s" % m.get_swap_payload_to_be_func_def()) + print("") + print("%s" % m.get_swap_payload_to_host_func_def()) + print("") + print("%s" % m.get_calc_msg_size_func_def()) + print("") + print("%s" % m.get_swap_to_be_func_def()) + print("") + print("%s" % m.get_swap_to_host_func_def()) + print("") + for m in parser.messages_by_json[j].values(): + if m.is_reply(): + continue + print("%s" % m.get_alloc_func_def()) + print("") + print("%s" % m.get_op_func_def()) + print("") + print("") + for m in parser.messages_by_json[j].values(): + print("%s" % m.get_c_constructor()) + print("") + print("") + for m in parser.messages_by_json[j].values(): + if not m.is_reply(): + continue + print("%s;" % m.get_event_cb_func_def()) + print("") + print("") + for m in parser.messages_by_json[j].values(): + print("vapi_msg_id_t %s;" % m.get_msg_id_name()) + sys.stdout = orig_stdout + + +def gen_json_unified_header(parser, logger, j, io): + logger.info("Generating header `%s'" % io.name) + orig_stdout = sys.stdout + sys.stdout = io + include_guard = "__included_%s" % ( + j.replace(".", "_").replace("/", "_").replace("-", "_")) + print("#ifndef %s" % include_guard) + print("#define %s" % include_guard) + print("") + print("#include ") + print("#include ") + print("#include ") + print("#include ") + print("#include ") + print("#include ") + if io.name == "vpe.api.vapi.h": + print("") + print("static inline vapi_error_e vapi_send_with_control_ping " + "(vapi_ctx_t ctx, void * msg, u32 context);") + else: + print("#include ") + print("") + for m in parser.messages_by_json[j].values(): + print("extern vapi_msg_id_t %s;" % m.get_msg_id_name()) + print("") + print("#define DEFINE_VAPI_MSG_IDS_%s\\" % + j.replace(".", "_").replace("/", "_").replace("-", "_").upper()) + print("\\\n".join([ + " vapi_msg_id_t %s;" % m.get_msg_id_name() + for m in parser.messages_by_json[j].values() + ])) + print("") + print("") + for t in parser.types_by_json[j].values(): + try: + print("%s" % t.get_c_def()) + print("") + except: + pass + for m in parser.messages_by_json[j].values(): + print("%s" % m.get_c_def()) + print("") + + print("") + function_attrs = "static inline " + for t in parser.types_by_json[j].values(): + print("%s%s" % (function_attrs, t.get_swap_to_be_func_def())) + print("") + print("%s%s" % (function_attrs, t.get_swap_to_host_func_def())) + print("") + for m in parser.messages_by_json[j].values(): + if m.has_payload(): + print("%s%s" % (function_attrs, + m.get_swap_payload_to_be_func_def())) + print("") + print("%s%s" % (function_attrs, + m.get_swap_payload_to_host_func_def())) + print("") + print("%s%s" % (function_attrs, m.get_calc_msg_size_func_def())) + print("") + print("%s%s" % (function_attrs, m.get_swap_to_be_func_def())) + print("") + print("%s%s" % (function_attrs, m.get_swap_to_host_func_def())) + print("") + for m in parser.messages_by_json[j].values(): + if m.is_reply(): + continue + print("%s%s" % (function_attrs, m.get_alloc_func_def())) + print("") + print("%s%s" % (function_attrs, m.get_op_func_def())) + print("") + print("") + for m in parser.messages_by_json[j].values(): + print("%s" % m.get_c_constructor()) + print("") + print("") + for m in parser.messages_by_json[j].values(): + if not m.is_reply(): + continue + print("%s%s;" % (function_attrs, m.get_event_cb_func_def())) + print("") + print("") + + if io.name == "vpe.api.vapi.h": + print("%s" % vapi_send_with_control_ping) + print("") + + print("#endif") + sys.stdout = orig_stdout + + +def json_to_header_name(json_name): + if json_name.endswith(".json"): + return "%s.vapi.h" % os.path.splitext(json_name)[0] + raise Exception("Unexpected json name `%s'!" % json_name) + + +def json_to_code_name(json_name): + if json_name.endswith(".json"): + return "%s.vapi.c" % os.path.splitext(json_name)[0] + raise Exception("Unexpected json name `%s'!" % json_name) + + +def gen_c_headers_and_code(parser, logger, prefix): + if prefix == "" or prefix is None: + prefix = "" + else: + prefix = "%s/" % prefix + for j in parser.json_files: + with open('%s%s' % (prefix, json_to_header_name(j)), "w") as io: + gen_json_header(parser, logger, j, io) + with open('%s%s' % (prefix, json_to_code_name(j)), "w") as io: + gen_json_code(parser, logger, j, io) + + +def gen_c_unified_headers(parser, logger, prefix): + if prefix == "" or prefix is None: + prefix = "" + else: + prefix = "%s/" % prefix + for j in parser.json_files: + with open('%s%s' % (prefix, json_to_header_name(j)), "w") as io: + gen_json_unified_header(parser, logger, j, io) + + +if __name__ == '__main__': + try: + verbose = int(os.getenv("V", 0)) + except: + verbose = 0 + + if verbose >= 2: + log_level = 10 + elif verbose == 1: + log_level = 20 + else: + log_level = 40 + + logging.basicConfig(stream=sys.stdout, level=log_level) + logger = logging.getLogger("VAPI C GEN") + logger.setLevel(log_level) + + argparser = argparse.ArgumentParser(description="VPP JSON API parser") + argparser.add_argument('files', metavar='api-file', action='append', + type=str, help='json api file' + '(may be specified multiple times)') + argparser.add_argument('--prefix', action='store', default=None, + help='path prefix') + args = argparser.parse_args() + + jsonparser = JsonParser(logger, args.files, + simple_type_class=CSimpleType, + struct_type_class=CStructType, + field_class=CField, + message_class=CMessage) + + # not using the model of having separate generated header and code files + # with generated symbols present in shared library (per discussion with + # Damjan), to avoid symbol version issues in .so + # gen_c_headers_and_code(jsonparser, logger, args.prefix) + + gen_c_unified_headers(jsonparser, logger, args.prefix) + + for e in jsonparser.exceptions: + logger.error(e) diff --git a/src/vpp-api/vapi/vapi_dbg.h b/src/vpp-api/vapi/vapi_dbg.h new file mode 100644 index 00000000..95a80089 --- /dev/null +++ b/src/vpp-api/vapi/vapi_dbg.h @@ -0,0 +1,76 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 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_vapi_debug_h__ +#define __included_vapi_debug_h__ + +/* controls debug prints */ +#define VAPI_DEBUG (0) +#define VAPI_DEBUG_CONNECT (0) +#define VAPI_DEBUG_ALLOC (0) + +#if VAPI_DEBUG +#include +#define VAPI_DEBUG_FILE_DEF \ + static const char *__file = NULL; \ + { \ + __file = strrchr (__FILE__, '/'); \ + if (__file) \ + { \ + ++__file; \ + } \ + else \ + { \ + __file = __FILE__; \ + } \ + } + +#define VAPI_DBG(fmt, ...) \ + do \ + { \ + VAPI_DEBUG_FILE_DEF \ + printf ("DBG:%s:%d:%s():" fmt, __file, __LINE__, __func__, \ + ##__VA_ARGS__); \ + printf ("\n"); \ + fflush (stdout); \ + } \ + while (0); + +#define VAPI_ERR(fmt, ...) \ + do \ + { \ + VAPI_DEBUG_FILE_DEF \ + printf ("ERR:%s:%d:%s():" fmt, __file, __LINE__, __func__, \ + ##__VA_ARGS__); \ + printf ("\n"); \ + fflush (stdout); \ + } \ + while (0); +#else +#define VAPI_DBG(...) +#define VAPI_ERR(...) +#endif + +#endif /* __included_vapi_debug_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/src/vpp-api/vapi/vapi_internal.h b/src/vpp-api/vapi/vapi_internal.h new file mode 100644 index 00000000..5b85788d --- /dev/null +++ b/src/vpp-api/vapi/vapi_internal.h @@ -0,0 +1,126 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 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 VAPI_INTERNAL_H +#define VAPI_INTERNAL_H + +#include +#include + +/** + * @file vapi_internal.h + * + * internal vpp api C declarations + * + * This file contains internal vpp api C declarations. It's not intended to be + * used by the client programmer and the API defined here might change at any + * time.. + */ + +struct vapi_ctx_s; + +typedef struct __attribute__ ((__packed__)) +{ + u16 _vl_msg_id; + u32 context; +} vapi_type_msg_header1_t; + +typedef struct __attribute__ ((__packed__)) +{ + u16 _vl_msg_id; + u32 client_index; + u32 context; +} vapi_type_msg_header2_t; + +static inline void +vapi_type_msg_header1_t_hton (vapi_type_msg_header1_t * h) +{ + h->_vl_msg_id = htobe16 (h->_vl_msg_id); +} + +static inline void +vapi_type_msg_header1_t_ntoh (vapi_type_msg_header1_t * h) +{ + h->_vl_msg_id = be16toh (h->_vl_msg_id); +} + +static inline void +vapi_type_msg_header2_t_hton (vapi_type_msg_header2_t * h) +{ + h->_vl_msg_id = htobe16 (h->_vl_msg_id); +} + +static inline void +vapi_type_msg_header2_t_ntoh (vapi_type_msg_header2_t * h) +{ + h->_vl_msg_id = be16toh (h->_vl_msg_id); +} + + +#include + +typedef vapi_error_e (*vapi_cb_t) (struct vapi_ctx_s *, void *, vapi_error_e, + bool, void *); + +typedef void (*generic_swap_fn_t) (void *payload); + +typedef struct +{ + const char *name; + size_t name_len; + const char *name_with_crc; + size_t name_with_crc_len; + bool has_context; + size_t context_offset; + size_t payload_offset; + size_t size; + generic_swap_fn_t swap_to_be; + generic_swap_fn_t swap_to_host; + vapi_msg_id_t id; /* assigned at run-time */ +} vapi_message_desc_t; + +typedef struct +{ + const char *name; + int payload_offset; + size_t size; + void (*swap_to_be) (void *payload); + void (*swap_to_host) (void *payload); +} vapi_event_desc_t; + +extern bool *__vapi_msg_is_with_context; + +vapi_msg_id_t vapi_register_msg (vapi_message_desc_t * msg); +u16 vapi_lookup_vl_msg_id (vapi_ctx_t ctx, vapi_msg_id_t id); +int vapi_get_client_index (vapi_ctx_t ctx); +bool vapi_is_nonblocking (vapi_ctx_t ctx); +bool vapi_requests_full (vapi_ctx_t ctx); +size_t vapi_get_request_count (vapi_ctx_t ctx); +size_t vapi_get_max_request_count (vapi_ctx_t ctx); +u32 vapi_gen_req_context (vapi_ctx_t ctx); +void vapi_store_request (vapi_ctx_t ctx, u32 context, bool is_dump, + vapi_cb_t callback, void *callback_ctx); +int vapi_get_payload_offset (vapi_msg_id_t id); +void (*vapi_get_swap_to_host_func (vapi_msg_id_t id)) (void *payload); +void (*vapi_get_swap_to_be_func (vapi_msg_id_t id)) (void *payload); +size_t vapi_get_message_size (vapi_msg_id_t id); +size_t vapi_get_context_offset (vapi_msg_id_t id); + +vapi_error_e vapi_producer_lock (vapi_ctx_t ctx); +vapi_error_e vapi_producer_unlock (vapi_ctx_t ctx); + +#endif diff --git a/src/vpp-api/vapi/vapi_json_parser.py b/src/vpp-api/vapi/vapi_json_parser.py new file mode 100644 index 00000000..57a22383 --- /dev/null +++ b/src/vpp-api/vapi/vapi_json_parser.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 + +import json + + +def msg_is_reply(name): + return name.endswith('_reply') or name.endswith('_details') \ + or name.endswith('_event') or name.endswith('_counters') + + +class ParseError (Exception): + pass + + +magic_prefix = "vl_api_" +magic_suffix = "_t" + + +def remove_magic(what): + if what.startswith(magic_prefix) and what.endswith(magic_suffix): + return what[len(magic_prefix): - len(magic_suffix)] + return what + + +class Field: + + def __init__( + self, + field_name, + field_type, + array_len=None, + nelem_field=None): + self.name = field_name + self.type = field_type + self.len = array_len + self.nelem_field = nelem_field + + def __str__(self): + if self.len is None: + return "name: %s, type: %s" % (self.name, self.type) + elif self.len > 0: + return "name: %s, type: %s, length: %s" % (self.name, self.type, + self.len) + else: + return ("name: %s, type: %s, variable length stored in: %s" % + (self.name, self.type, self.nelem_field)) + + +class Type: + def __init__(self, name): + self.name = name + + +class SimpleType (Type): + + def __init__(self, name): + super().__init__(name) + + def __str__(self): + return self.name + + +def get_msg_header_defs(struct_type_class, field_class, typedict): + return [ + struct_type_class(['msg_header1_t', + ['u16', '_vl_msg_id'], + ['u32', 'context'], + ], + typedict, field_class + ), + struct_type_class(['msg_header2_t', + ['u16', '_vl_msg_id'], + ['u32', 'client_index'], + ['u32', 'context'], + ], + typedict, field_class + ), + ] + + +class Struct: + + def __init__(self, name, fields): + self.name = name + self.fields = fields + self.field_names = [n.name for n in self.fields] + + +class Message: + + def __init__(self, logger, definition, typedict, + struct_type_class, simple_type_class, field_class): + self.logger = logger + m = definition + logger.debug("Parsing message definition `%s'" % m) + name = m[0] + self.name = name + logger.debug("Message name is `%s'" % name) + ignore = True + self.header = None + fields = [] + for header in get_msg_header_defs(struct_type_class, field_class, + typedict): + logger.debug("Probing header `%s'" % header.name) + if header.is_part_of_def(m[1:]): + self.header = header + logger.debug("Found header `%s'" % header.name) + fields.append(field_class(field_name='header', + field_type=self.header)) + ignore = False + break + if ignore and not msg_is_reply(name): + raise ParseError("While parsing message `%s': could not find all " + "common header fields" % name) + for field in m[1:]: + if len(field) == 1 and 'crc' in field: + self.crc = field['crc'] + logger.debug("Found CRC `%s'" % self.crc) + continue + else: + field_type = field[0] + if field_type in typedict: + field_type = typedict[field_type] + else: + field_type = typedict[remove_magic(field_type)] + if len(field) == 2: + if self.header is not None and\ + self.header.has_field(field[1]): + continue + p = field_class(field_name=field[1], + field_type=field_type) + elif len(field) == 3: + if field[2] == 0: + raise ParseError( + "While parsing message `%s': variable length " + "array `%s' doesn't have reference to member " + "containing the actual length" % ( + name, field[1])) + p = field_class( + field_name=field[1], + field_type=field_type, + array_len=field[2]) + elif len(field) == 4: + nelem_field = None + for f in fields: + if f.name == field[3]: + nelem_field = f + if nelem_field is None: + raise ParseError( + "While parsing message `%s': couldn't find " + "variable length array `%s' member containing " + "the actual length `%s'" % ( + name, field[1], field[3])) + p = field_class( + field_name=field[1], + field_type=field_type, + array_len=field[2], + nelem_field=nelem_field) + else: + raise Exception("Don't know how to parse message " + "definition for message `%s': `%s'" % + (m, m[1:])) + logger.debug("Parsed field `%s'" % p) + fields.append(p) + self.fields = fields + + def is_dump(self): + return self.name.endswith('_dump') + + def is_reply(self): + return msg_is_reply(self.name) + + +class StructType (Type, Struct): + + def __init__(self, definition, typedict, field_class): + t = definition + name = t[0] + fields = [] + for field in t[1:]: + if len(field) == 1 and 'crc' in field: + self.crc = field['crc'] + continue + elif len(field) == 2: + p = field_class(field_name=field[1], + field_type=typedict[field[0]]) + elif len(field) == 3: + if field[2] == 0: + raise ParseError("While parsing type `%s': array `%s' has " + "variable length" % (name, field[1])) + p = field_class(field_name=field[1], + field_type=typedict[field[0]], + array_len=field[2]) + else: + raise ParseError( + "Don't know how to parse type definition for " + "type `%s': `%s'" % (t, t[1:])) + fields.append(p) + Type.__init__(self, name) + Struct.__init__(self, name, fields) + + def has_field(self, name): + return name in self.field_names + + def is_part_of_def(self, definition): + for idx in range(len(self.fields)): + field = definition[idx] + p = self.fields[idx] + if field[1] != p.name: + return False + if field[0] != p.type.name: + raise ParseError( + "Unexpected field type `%s' (should be `%s'), " + "while parsing msg/def/field `%s/%s/%s'" % + (field[0], p.type, p.name, definition, field)) + return True + + +class JsonParser: + def __init__(self, logger, files, simple_type_class=SimpleType, + struct_type_class=StructType, field_class=Field, + message_class=Message): + self.messages = {} + self.types = { + x: simple_type_class(x) for x in [ + 'i8', 'i16', 'i32', 'i64', + 'u8', 'u16', 'u32', 'u64', + 'f64' + ] + } + + self.simple_type_class = simple_type_class + self.struct_type_class = struct_type_class + self.field_class = field_class + self.message_class = message_class + + self.exceptions = [] + self.json_files = [] + self.types_by_json = {} + self.messages_by_json = {} + self.logger = logger + for f in files: + self.parse_json_file(f) + self.finalize_parsing() + + def parse_json_file(self, path): + self.logger.info("Parsing json api file: `%s'" % path) + self.json_files.append(path) + self.types_by_json[path] = {} + self.messages_by_json[path] = {} + with open(path) as f: + j = json.load(f) + for t in j['types']: + try: + type_ = self.struct_type_class(t, self.types, + self.field_class) + if type_.name in self.types: + raise ParseError("Duplicate type `%s'" % type_.name) + except ParseError as e: + self.exceptions.append(e) + continue + self.types[type_.name] = type_ + self.types_by_json[path][type_.name] = type_ + for m in j['messages']: + try: + msg = self.message_class(self.logger, m, self.types, + self.struct_type_class, + self.simple_type_class, + self.field_class) + if msg.name in self.messages: + raise ParseError("Duplicate message `%s'" % msg.name) + except ParseError as e: + self.exceptions.append(e) + continue + self.messages[msg.name] = msg + self.messages_by_json[path][msg.name] = msg + + def get_reply(self, message): + if self.messages[message].is_dump(): + return self.messages["%s_details" % message[:-len("_dump")]] + return self.messages["%s_reply" % message] + + def finalize_parsing(self): + if len(self.messages) == 0: + for e in self.exceptions: + self.logger.error(e) + raise Exception("No messages parsed.") + for jn, j in self.messages_by_json.items(): + remove = [] + for n, m in j.items(): + try: + if not m.is_reply(): + try: + m.reply = self.get_reply(n) + except: + raise ParseError( + "Cannot find reply to message `%s'" % n) + except ParseError as e: + self.exceptions.append(e) + remove.append(n) + + self.messages_by_json[jn] = { + k: v for k, v in j.items() if k not in remove} diff --git a/test/Makefile b/test/Makefile index 72b4dac7..132ebee6 100644 --- a/test/Makefile +++ b/test/Makefile @@ -107,7 +107,11 @@ sanity: verify-no-running-vpp echo \"*******************************************************************\" &&\ false)" -test: verify-python-path $(PAPI_INSTALL_DONE) sanity reset +.PHONY: ext +ext: + make -C ext + +test: verify-python-path $(PAPI_INSTALL_DONE) ext sanity reset $(call retest-func) retest: verify-python-path sanity reset diff --git a/test/ext/Makefile b/test/ext/Makefile new file mode 100644 index 00000000..4a45fef6 --- /dev/null +++ b/test/ext/Makefile @@ -0,0 +1,17 @@ +BINDIR = $(BR)/vapi_test/ +BIN = $(addprefix $(BINDIR), vapi_test) +LIBS = -L$(VPP_TEST_BUILD_DIR)/vpp/.libs/ -L$(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi/.libs/ -lvppinfra -lvlibmemoryclient -lsvm -lpthread -lcheck -lsubunit -lrt -lm -lvapiclient +CFLAGS = -ggdb -O0 -Wall -pthread -I$(WS_ROOT)/src -I$(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi -I$(WS_ROOT)/src/vpp-api/vapi/ + +all: $(BIN) + +$(BINDIR): + mkdir -p $(BINDIR) + +SRC = vapi_test.c + +$(BIN): $(SRC) $(BINDIR) $(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi/.libs/libvapiclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvppinfra.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvlibmemoryclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libsvm.so + gcc -ggdb -o $@ $(SRC) $(CFLAGS) $(LIBS) + +clean: + rm -rf $(BINDIR) diff --git a/test/ext/vapi_test.c b/test/ext/vapi_test.c new file mode 100644 index 00000000..eca6be7d --- /dev/null +++ b/test/ext/vapi_test.c @@ -0,0 +1,1152 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 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. + *------------------------------------------------------------------ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DEFINE_VAPI_MSG_IDS_VPE_API_JSON; +DEFINE_VAPI_MSG_IDS_INTERFACE_API_JSON; +DEFINE_VAPI_MSG_IDS_L2_API_JSON; +DEFINE_VAPI_MSG_IDS_STATS_API_JSON; + +static char *app_name = NULL; +static char *api_prefix = NULL; +static const int max_outstanding_requests = 64; +static const int response_queue_size = 32; + +START_TEST (test_invalid_values) +{ + vapi_ctx_t ctx; + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_eq (NULL, sv); + rv = vapi_send (ctx, sv); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_send (ctx, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_send (NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_recv (NULL, NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_recv (ctx, NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + vapi_msg_show_version_reply *reply; + rv = vapi_recv (ctx, (void **) &reply, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_disconnect (ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_ctx_free (ctx); +} + +END_TEST; + +START_TEST (test_hton_1) +{ + const u16 _vl_msg_id = 1; + vapi_type_msg_header1_t h; + h._vl_msg_id = _vl_msg_id; + vapi_type_msg_header1_t_hton (&h); + ck_assert_int_eq (be16toh (h._vl_msg_id), _vl_msg_id); +} + +END_TEST; + +START_TEST (test_hton_2) +{ + const u16 _vl_msg_id = 1; + const u32 client_index = 3; + vapi_type_msg_header2_t h; + h._vl_msg_id = _vl_msg_id; + h.client_index = client_index; + vapi_type_msg_header2_t_hton (&h); + ck_assert_int_eq (be16toh (h._vl_msg_id), _vl_msg_id); + ck_assert_int_eq (h.client_index, client_index); +} + +END_TEST; + +START_TEST (test_hton_3) +{ + const size_t data_size = 10; + vapi_msg_vnet_interface_combined_counters *m = + malloc (sizeof (vapi_msg_vnet_interface_combined_counters) + + data_size * sizeof (vapi_type_vlib_counter)); + ck_assert_ptr_ne (NULL, m); + vapi_payload_vnet_interface_combined_counters *p = &m->payload; + const u16 _vl_msg_id = 1; + p->_vl_msg_id = _vl_msg_id; + const u32 first_sw_if_index = 2; + p->first_sw_if_index = first_sw_if_index; + p->count = data_size; + const u64 packets = 1234; + const u64 bytes = 2345; + int i; + for (i = 0; i < data_size; ++i) + { + p->data[i].packets = packets; + p->data[i].bytes = bytes; + } + vapi_msg_vnet_interface_combined_counters_hton (m); + ck_assert_int_eq (_vl_msg_id, be16toh (p->_vl_msg_id)); + ck_assert_int_eq (first_sw_if_index, be32toh (p->first_sw_if_index)); + ck_assert_int_eq (data_size, be32toh (p->count)); + for (i = 0; i < data_size; ++i) + { + ck_assert_int_eq (packets, be64toh (p->data[i].packets)); + ck_assert_int_eq (bytes, be64toh (p->data[i].bytes)); + } + free (p); +} + +END_TEST; + +#define verify_hton_swap(expr, value) \ + if (4 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, htobe32 (value)); \ + } \ + else if (2 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, htobe16 (value)); \ + } \ + else \ + { \ + ck_assert_int_eq (expr, value); \ + } + +START_TEST (test_hton_4) +{ + const int vla_count = 3; + char x[sizeof (vapi_msg_bridge_domain_details) + + vla_count * sizeof (vapi_type_bridge_domain_sw_if)]; + vapi_msg_bridge_domain_details *d = (void *) x; + int cnt = 1; + d->header._vl_msg_id = cnt++; + d->header.context = cnt++; + d->payload.bd_id = cnt++; + d->payload.flood = cnt++; + d->payload.uu_flood = cnt++; + d->payload.forward = cnt++; + d->payload.learn = cnt++; + d->payload.arp_term = cnt++; + d->payload.mac_age = cnt++; + d->payload.bvi_sw_if_index = cnt++; + d->payload.n_sw_ifs = vla_count; + int i; + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + det->context = cnt++; + det->sw_if_index = cnt++; + det->shg = cnt++; + } + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); + vapi_msg_bridge_domain_details_hton (d); + int tmp = 1; + verify_hton_swap (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + verify_hton_swap (d->payload.bd_id, tmp); + ++tmp; + verify_hton_swap (d->payload.flood, tmp); + ++tmp; + verify_hton_swap (d->payload.uu_flood, tmp); + ++tmp; + verify_hton_swap (d->payload.forward, tmp); + ++tmp; + verify_hton_swap (d->payload.learn, tmp); + ++tmp; + verify_hton_swap (d->payload.arp_term, tmp); + ++tmp; + verify_hton_swap (d->payload.mac_age, tmp); + ++tmp; + verify_hton_swap (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, htobe32 (vla_count)); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + verify_hton_swap (det->context, tmp); + ++tmp; + verify_hton_swap (det->sw_if_index, tmp); + ++tmp; + verify_hton_swap (det->shg, tmp); + ++tmp; + } + vapi_msg_bridge_domain_details_ntoh (d); + tmp = 1; + ck_assert_int_eq (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bd_id, tmp); + ++tmp; + ck_assert_int_eq (d->payload.flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.uu_flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.forward, tmp); + ++tmp; + ck_assert_int_eq (d->payload.learn, tmp); + ++tmp; + ck_assert_int_eq (d->payload.arp_term, tmp); + ++tmp; + ck_assert_int_eq (d->payload.mac_age, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, vla_count); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + ck_assert_int_eq (det->context, tmp); + ++tmp; + ck_assert_int_eq (det->sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (det->shg, tmp); + ++tmp; + } + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); +} + +END_TEST; + +START_TEST (test_ntoh_1) +{ + const u16 _vl_msg_id = 1; + vapi_type_msg_header1_t h; + h._vl_msg_id = _vl_msg_id; + vapi_type_msg_header1_t_ntoh (&h); + ck_assert_int_eq (htobe16 (h._vl_msg_id), _vl_msg_id); +} + +END_TEST; + +START_TEST (test_ntoh_2) +{ + const u16 _vl_msg_id = 1; + const u32 client_index = 3; + vapi_type_msg_header2_t h; + h._vl_msg_id = _vl_msg_id; + h.client_index = client_index; + vapi_type_msg_header2_t_ntoh (&h); + ck_assert_int_eq (htobe16 (h._vl_msg_id), _vl_msg_id); + ck_assert_int_eq (h.client_index, client_index); +} + +END_TEST; + +START_TEST (test_ntoh_3) +{ + const size_t data_size = 10; + vapi_msg_vnet_interface_combined_counters *m = + malloc (sizeof (vapi_msg_vnet_interface_combined_counters) + + data_size * sizeof (vapi_type_vlib_counter)); + ck_assert_ptr_ne (NULL, m); + vapi_payload_vnet_interface_combined_counters *p = &m->payload; + const u16 _vl_msg_id = 1; + p->_vl_msg_id = _vl_msg_id; + const u32 first_sw_if_index = 2; + p->first_sw_if_index = first_sw_if_index; + const size_t be_data_size = htobe32 (data_size); + p->count = be_data_size; + const u64 packets = 1234; + const u64 bytes = 2345; + int i; + for (i = 0; i < data_size; ++i) + { + p->data[i].packets = packets; + p->data[i].bytes = bytes; + } + vapi_msg_vnet_interface_combined_counters_ntoh (m); + ck_assert_int_eq (_vl_msg_id, be16toh (p->_vl_msg_id)); + ck_assert_int_eq (first_sw_if_index, be32toh (p->first_sw_if_index)); + ck_assert_int_eq (be_data_size, be32toh (p->count)); + for (i = 0; i < data_size; ++i) + { + ck_assert_int_eq (packets, htobe64 (p->data[i].packets)); + ck_assert_int_eq (bytes, htobe64 (p->data[i].bytes)); + } + free (p); +} + +END_TEST; + +#define verify_ntoh_swap(expr, value) \ + if (4 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, be32toh (value)); \ + } \ + else if (2 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, be16toh (value)); \ + } \ + else \ + { \ + ck_assert_int_eq (expr, value); \ + } + +START_TEST (test_ntoh_4) +{ + const int vla_count = 3; + char x[sizeof (vapi_msg_bridge_domain_details) + + vla_count * sizeof (vapi_type_bridge_domain_sw_if)]; + vapi_msg_bridge_domain_details *d = (void *) x; + int cnt = 1; + d->header._vl_msg_id = cnt++; + d->header.context = cnt++; + d->payload.bd_id = cnt++; + d->payload.flood = cnt++; + d->payload.uu_flood = cnt++; + d->payload.forward = cnt++; + d->payload.learn = cnt++; + d->payload.arp_term = cnt++; + d->payload.mac_age = cnt++; + d->payload.bvi_sw_if_index = cnt++; + d->payload.n_sw_ifs = htobe32 (vla_count); + int i; + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + det->context = cnt++; + det->sw_if_index = cnt++; + det->shg = cnt++; + } + vapi_msg_bridge_domain_details_ntoh (d); + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); + int tmp = 1; + verify_ntoh_swap (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + verify_ntoh_swap (d->payload.bd_id, tmp); + ++tmp; + verify_ntoh_swap (d->payload.flood, tmp); + ++tmp; + verify_ntoh_swap (d->payload.uu_flood, tmp); + ++tmp; + verify_ntoh_swap (d->payload.forward, tmp); + ++tmp; + verify_ntoh_swap (d->payload.learn, tmp); + ++tmp; + verify_ntoh_swap (d->payload.arp_term, tmp); + ++tmp; + verify_ntoh_swap (d->payload.mac_age, tmp); + ++tmp; + verify_ntoh_swap (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, vla_count); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + verify_ntoh_swap (det->context, tmp); + ++tmp; + verify_ntoh_swap (det->sw_if_index, tmp); + ++tmp; + verify_ntoh_swap (det->shg, tmp); + ++tmp; + } + vapi_msg_bridge_domain_details_hton (d); + tmp = 1; + ck_assert_int_eq (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bd_id, tmp); + ++tmp; + ck_assert_int_eq (d->payload.flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.uu_flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.forward, tmp); + ++tmp; + ck_assert_int_eq (d->payload.learn, tmp); + ++tmp; + ck_assert_int_eq (d->payload.arp_term, tmp); + ++tmp; + ck_assert_int_eq (d->payload.mac_age, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, htobe32 (vla_count)); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + ck_assert_int_eq (det->context, tmp); + ++tmp; + ck_assert_int_eq (det->sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (det->shg, tmp); + ++tmp; + } +} + +END_TEST; + +vapi_error_e +show_version_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_show_version_reply * p) +{ + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (true, is_last); + ck_assert_str_eq ("vpe", (char *) p->program); + printf + ("show_version_reply: program: `%s', version: `%s', build directory: " + "`%s', build date: `%s'\n", p->program, p->version, p->build_directory, + p->build_date); + ++*(int *) caller_ctx; + return VAPI_OK; +} + +typedef struct +{ + int called; + int expected_retval; + u32 *sw_if_index_storage; +} test_create_loopback_ctx_t; + +vapi_error_e +loopback_create_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_create_loopback_reply * p) +{ + test_create_loopback_ctx_t *clc = caller_ctx; + ck_assert_int_eq (clc->expected_retval, p->retval); + *clc->sw_if_index_storage = p->sw_if_index; + ++clc->called; + return VAPI_OK; +} + +typedef struct +{ + int called; + int expected_retval; + u32 *sw_if_index_storage; +} test_delete_loopback_ctx_t; + +vapi_error_e +loopback_delete_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_delete_loopback_reply * p) +{ + test_delete_loopback_ctx_t *dlc = caller_ctx; + ck_assert_int_eq (dlc->expected_retval, p->retval); + ++dlc->called; + return VAPI_OK; +} + +START_TEST (test_connect) +{ + vapi_ctx_t ctx; + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_disconnect (ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_ctx_free (ctx); +} + +END_TEST; + +vapi_ctx_t ctx; + +void +setup_blocking (void) +{ + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); +} + +void +setup_nonblocking (void) +{ + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_NONBLOCKING); + ck_assert_int_eq (VAPI_OK, rv); +} + +void +teardown (void) +{ + vapi_disconnect (ctx); + vapi_ctx_free (ctx); +} + +START_TEST (test_show_version_1) +{ + printf ("--- Basic show version message - reply test ---\n"); + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + vapi_error_e rv = vapi_send (ctx, sv); + ck_assert_int_eq (VAPI_OK, rv); + vapi_msg_show_version_reply *resp; + size_t size; + rv = vapi_recv (ctx, (void *) &resp, &size); + ck_assert_int_eq (VAPI_OK, rv); + vapi_payload_show_version_reply *payload = &resp->payload; + int dummy; + show_version_cb (NULL, &dummy, VAPI_OK, true, payload); + vapi_msg_free (ctx, resp); +} + +END_TEST; + +START_TEST (test_show_version_2) +{ + int called = 0; + printf ("--- Show version via blocking callback API ---\n"); + const int attempts = response_queue_size * 4; + int i = 0; + for (i = 0; i < attempts; ++i) + { + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_error_e rv = vapi_show_version (ctx, sv, show_version_cb, &called); + ck_assert_int_eq (VAPI_OK, rv); + } + ck_assert_int_eq (attempts, called); +} + +END_TEST; + +typedef struct +{ + bool last_called; + size_t num_ifs; + u32 *sw_if_indexes; + bool *seen; + int called; +} sw_interface_dump_ctx; + +vapi_error_e +sw_interface_dump_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_sw_interface_details * reply) +{ + sw_interface_dump_ctx *dctx = callback_ctx; + ck_assert_int_eq (false, dctx->last_called); + if (is_last) + { + ck_assert (NULL == reply); + dctx->last_called = true; + } + else + { + ck_assert (reply); + printf ("Interface dump entry: [%u]: %s\n", reply->sw_if_index, + reply->interface_name); + size_t i = 0; + for (i = 0; i < dctx->num_ifs; ++i) + { + if (dctx->sw_if_indexes[i] == reply->sw_if_index) + { + ck_assert_int_eq (false, dctx->seen[i]); + dctx->seen[i] = true; + } + } + } + ++dctx->called; + return VAPI_OK; +} + +START_TEST (test_loopbacks_1) +{ + printf ("--- Create/delete loopbacks using blocking API ---\n"); + const size_t num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + u32 sw_if_indexes[num_ifs]; + memset (&sw_if_indexes, 0xff, sizeof (sw_if_indexes)); + test_create_loopback_ctx_t clcs[num_ifs]; + memset (&clcs, 0, sizeof (clcs)); + test_delete_loopback_ctx_t dlcs[num_ifs]; + memset (&dlcs, 0, sizeof (dlcs)); + int i; + for (i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + clcs[i].sw_if_index_storage = &sw_if_indexes[i]; + } + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_create_loopback *cl = vapi_alloc_create_loopback (ctx); + memcpy (cl->payload.mac_address, mac_addresses[i], + sizeof (cl->payload.mac_address)); + vapi_error_e rv = + vapi_create_loopback (ctx, cl, loopback_create_cb, &clcs[i]); + ck_assert_int_eq (VAPI_OK, rv); + } + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, clcs[i].called); + printf ("Created loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + sw_if_indexes[i]); + } + bool seen[num_ifs]; + sw_interface_dump_ctx dctx = { false, num_ifs, sw_if_indexes, seen, 0 }; + vapi_msg_sw_interface_dump *dump; + vapi_error_e rv; + const int attempts = response_queue_size * 4; + for (i = 0; i < attempts; ++i) + { + dctx.last_called = false; + memset (&seen, 0, sizeof (seen)); + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, + sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, + &dctx))) + ; + ck_assert_int_eq (true, dctx.last_called); + int j = 0; + for (j = 0; j < num_ifs; ++j) + { + ck_assert_int_eq (true, seen[j]); + } + } + memset (&seen, 0, sizeof (seen)); + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_delete_loopback *dl = vapi_alloc_delete_loopback (ctx); + dl->payload.sw_if_index = sw_if_indexes[i]; + vapi_error_e rv = + vapi_delete_loopback (ctx, dl, loopback_delete_cb, &dlcs[i]); + ck_assert_int_eq (VAPI_OK, rv); + } + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, dlcs[i].called); + printf ("Deleted loopback with sw_if_index %u\n", sw_if_indexes[i]); + } + dctx.last_called = false; + memset (&seen, 0, sizeof (seen)); + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + ck_assert_int_eq (true, dctx.last_called); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } +} + +END_TEST; + +START_TEST (test_show_version_3) +{ + printf ("--- Show version via async callback ---\n"); + int called = 0; + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (0, called); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); + called = 0; + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (0, called); +} + +END_TEST; + +START_TEST (test_show_version_4) +{ + printf ("--- Show version via async callback - multiple messages ---\n"); + vapi_error_e rv; + const size_t num_req = 5; + int contexts[num_req]; + memset (contexts, 0, sizeof (contexts)); + int i; + for (i = 0; i < num_req; ++i) + { + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = + vapi_show_version (ctx, sv, show_version_cb, &contexts[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + int j; + for (j = 0; j < num_req; ++j) + { + ck_assert_int_eq (0, contexts[j]); + } + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_req; ++i) + { + ck_assert_int_eq (1, contexts[i]); + } + memset (contexts, 0, sizeof (contexts)); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_req; ++i) + { + ck_assert_int_eq (0, contexts[i]); + } +} + +END_TEST; + +START_TEST (test_loopbacks_2) +{ + printf ("--- Create/delete loopbacks using non-blocking API ---\n"); + vapi_error_e rv; + const size_t num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + u32 sw_if_indexes[num_ifs]; + memset (&sw_if_indexes, 0xff, sizeof (sw_if_indexes)); + test_create_loopback_ctx_t clcs[num_ifs]; + memset (&clcs, 0, sizeof (clcs)); + test_delete_loopback_ctx_t dlcs[num_ifs]; + memset (&dlcs, 0, sizeof (dlcs)); + int i; + for (i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + clcs[i].sw_if_index_storage = &sw_if_indexes[i]; + } + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_create_loopback *cl = vapi_alloc_create_loopback (ctx); + memcpy (cl->payload.mac_address, mac_addresses[i], + sizeof (cl->payload.mac_address)); + while (VAPI_EAGAIN == + (rv = + vapi_create_loopback (ctx, cl, loopback_create_cb, &clcs[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, clcs[i].called); + printf ("Loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + sw_if_indexes[i]); + } + bool seen[num_ifs]; + memset (&seen, 0, sizeof (seen)); + sw_interface_dump_ctx dctx = { false, num_ifs, sw_if_indexes, seen, 0 }; + vapi_msg_sw_interface_dump *dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (false, dctx.last_called); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (true, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (true, dctx.last_called); + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_delete_loopback *dl = vapi_alloc_delete_loopback (ctx); + dl->payload.sw_if_index = sw_if_indexes[i]; + while (VAPI_EAGAIN == + (rv = + vapi_delete_loopback (ctx, dl, loopback_delete_cb, &dlcs[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, dlcs[i].called); + printf ("Deleted loopback with sw_if_index %u\n", sw_if_indexes[i]); + } + memset (&seen, 0, sizeof (seen)); + dctx.last_called = false; + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (true, dctx.last_called); +} + +END_TEST; + +vapi_error_e +interface_simple_stats_cb (vapi_ctx_t ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_want_interface_simple_stats_reply * + payload) +{ + return VAPI_OK; +} + +vapi_error_e +simple_counters_cb (vapi_ctx_t ctx, void *callback_ctx, + vapi_payload_vnet_interface_simple_counters * payload) +{ + int *called = callback_ctx; + ++*called; + printf ("simple counters: first_sw_if_index=%u\n", + payload->first_sw_if_index); + return VAPI_OK; +} + +START_TEST (test_stats_1) +{ + printf ("--- Receive stats using generic blocking API ---\n"); + vapi_msg_want_interface_simple_stats *ws = + vapi_alloc_want_interface_simple_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_interface_simple_stats (ctx, ws, interface_simple_stats_cb, + NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_event_cb (ctx, vapi_msg_id_vnet_interface_simple_counters, + (vapi_event_cb) simple_counters_cb, &called); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); +} + +END_TEST; + +START_TEST (test_stats_2) +{ + printf ("--- Receive stats using stat-specific blocking API ---\n"); + vapi_msg_want_interface_simple_stats *ws = + vapi_alloc_want_interface_simple_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_interface_simple_stats (ctx, ws, interface_simple_stats_cb, + NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_vapi_msg_vnet_interface_simple_counters_event_cb (ctx, + simple_counters_cb, + &called); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); +} + +END_TEST; + +vapi_error_e +generic_cb (vapi_ctx_t ctx, void *callback_ctx, vapi_msg_id_t id, void *msg) +{ + int *called = callback_ctx; + ck_assert_int_eq (0, *called); + ++*called; + ck_assert_int_eq (id, vapi_msg_id_show_version_reply); + ck_assert_ptr_ne (NULL, msg); + vapi_msg_show_version_reply *reply = msg; + ck_assert_str_eq ("vpe", (char *) reply->payload.program); + return VAPI_OK; +} + +START_TEST (test_show_version_5) +{ + printf ("--- Receive show version using generic callback - nonblocking " + "API ---\n"); + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + while (VAPI_EAGAIN == (rv = vapi_send (ctx, sv))) + ; + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_generic_event_cb (ctx, generic_cb, &called); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); + sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + while (VAPI_EAGAIN == (rv = vapi_send (ctx, sv))) + ; + ck_assert_int_eq (VAPI_OK, rv); + vapi_clear_generic_event_cb (ctx); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); /* needs to remain unchanged */ +} + +END_TEST; + +vapi_error_e +combined_counters_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_payload_vnet_interface_combined_counters * payload) +{ + int *called = callback_ctx; + ++*called; + printf ("combined counters: first_sw_if_index=%u\n", + payload->first_sw_if_index); + return VAPI_OK; +} + +vapi_error_e +stats_cb (vapi_ctx_t ctx, void *callback_ctx, vapi_error_e rv, + bool is_last, vapi_payload_want_stats_reply * payload) +{ + return VAPI_OK; +} + +START_TEST (test_stats_3) +{ + printf ("--- Receive multiple stats using stat-specific non-blocking API " + "---\n"); + vapi_msg_want_stats *ws = vapi_alloc_want_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_stats (ctx, ws, stats_cb, NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + int called2 = 0; + vapi_set_vapi_msg_vnet_interface_simple_counters_event_cb (ctx, + simple_counters_cb, + &called); + vapi_set_vapi_msg_vnet_interface_combined_counters_event_cb (ctx, + combined_counters_cb, + &called2); + while (!called || !called2) + { + if (VAPI_EAGAIN != (rv = vapi_dispatch_one (ctx))) + { + ck_assert_int_eq (VAPI_OK, rv); + } + } +} + +END_TEST; + +vapi_error_e +show_version_no_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_show_version_reply * p) +{ + ck_assert_int_eq (VAPI_ENORESP, rv); + ck_assert_int_eq (true, is_last); + ck_assert_ptr_eq (NULL, p); + ++*(int *) caller_ctx; + return VAPI_OK; +} + +START_TEST (test_no_response_1) +{ + printf ("--- Simulate no response to regular message ---\n"); + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + sv->header._vl_msg_id = ~0; /* malformed ID causes vpp to drop the msg */ + int called = 0; + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_no_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (2, called); +} + +END_TEST; + +vapi_error_e +no_msg_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_sw_interface_details * reply) +{ + int *called = callback_ctx; + ++*called; + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (true, is_last); + ck_assert_ptr_eq (NULL, reply); + return VAPI_OK; +} + +START_TEST (test_no_response_2) +{ + printf ("--- Simulate no response to dump message ---\n"); + vapi_error_e rv; + vapi_msg_sw_interface_dump *dump = vapi_alloc_sw_interface_dump (ctx); + dump->header._vl_msg_id = ~0; /* malformed ID causes vpp to drop the msg */ + int no_called = 0; + while (VAPI_EAGAIN == + (rv = vapi_sw_interface_dump (ctx, dump, no_msg_cb, &no_called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, no_called); +} + +END_TEST; +Suite * +test_suite (void) +{ + Suite *s = suite_create ("VAPI test"); + + TCase *tc_negative = tcase_create ("Negative tests"); + tcase_add_test (tc_negative, test_invalid_values); + suite_add_tcase (s, tc_negative); + + TCase *tc_swap = tcase_create ("Byteswap tests"); + tcase_add_test (tc_swap, test_hton_1); + tcase_add_test (tc_swap, test_hton_2); + tcase_add_test (tc_swap, test_hton_3); + tcase_add_test (tc_swap, test_hton_4); + tcase_add_test (tc_swap, test_ntoh_1); + tcase_add_test (tc_swap, test_ntoh_2); + tcase_add_test (tc_swap, test_ntoh_3); + tcase_add_test (tc_swap, test_ntoh_4); + suite_add_tcase (s, tc_swap); + + TCase *tc_connect = tcase_create ("Connect"); + tcase_add_test (tc_connect, test_connect); + suite_add_tcase (s, tc_connect); + + TCase *tc_block = tcase_create ("Blocking API"); + tcase_set_timeout (tc_block, 25); + tcase_add_checked_fixture (tc_block, setup_blocking, teardown); + tcase_add_test (tc_block, test_show_version_1); + tcase_add_test (tc_block, test_show_version_2); + tcase_add_test (tc_block, test_loopbacks_1); + tcase_add_test (tc_block, test_stats_1); + tcase_add_test (tc_block, test_stats_2); + suite_add_tcase (s, tc_block); + + TCase *tc_nonblock = tcase_create ("Nonblocking API"); + tcase_set_timeout (tc_nonblock, 25); + tcase_add_checked_fixture (tc_nonblock, setup_nonblocking, teardown); + tcase_add_test (tc_nonblock, test_show_version_3); + tcase_add_test (tc_nonblock, test_show_version_4); + tcase_add_test (tc_nonblock, test_show_version_5); + tcase_add_test (tc_nonblock, test_loopbacks_2); + tcase_add_test (tc_nonblock, test_stats_3); + tcase_add_test (tc_nonblock, test_no_response_1); + tcase_add_test (tc_nonblock, test_no_response_2); + suite_add_tcase (s, tc_nonblock); + + return s; +} + +int +main (int argc, char *argv[]) +{ + if (3 != argc) + { + printf ("Invalid argc==`%d'\n", argc); + return EXIT_FAILURE; + } + app_name = argv[1]; + api_prefix = argv[2]; + printf ("App name: `%s', API prefix: `%s'\n", app_name, api_prefix); + + int number_failed; + Suite *s; + SRunner *sr; + + s = test_suite (); + sr = srunner_create (s); + + srunner_run_all (sr, CK_NORMAL); + number_failed = srunner_ntests_failed (sr); + srunner_free (sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/test/scripts/test-loop.sh b/test/scripts/test-loop.sh index 17dc7c39..51f5d5ce 100755 --- a/test/scripts/test-loop.sh +++ b/test/scripts/test-loop.sh @@ -3,14 +3,15 @@ function usage() { echo "$0" 1>&2 echo "" 1>&2 - echo "Usage: $0 [-p ] [-m ] -- " 1>&2 + echo "Usage: $0 [-p ] [-m ] -- " 1>&2 echo "" 1>&2 echo "Parameters:" 1>&2 echo " -p - run a command before each test loop (e.g. 'git pull')" 1>&2 echo " -m - if set, email is sent to this address on failure" 1>&2 echo "" 1>&2 - echo "Example:" 1>&2 - echo " $0 -m -- test-debug TEST=l2bd" + echo "Examples:" 1>&2 + echo " $0 -m -- test-debug TEST=l2bd" 1>&2 + echo " $0 -m -- verify" 1>&2 exit 1; } @@ -44,8 +45,11 @@ shift $((OPTIND-1)) if ! echo $* | grep test >/dev/null then - echo "Error: command line doesn't look right - should contain \`test' token..." >&2 - usage + if ! echo $* | grep verify >/dev/null + then + echo "Error: command line doesn't look right - should contain \`test' or \`verify' token..." >&2 + usage + fi fi function finish { diff --git a/test/test_vapi.py b/test/test_vapi.py new file mode 100644 index 00000000..86c1ee06 --- /dev/null +++ b/test/test_vapi.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +""" VAPI test """ + +from __future__ import division +import unittest +import os +import signal +import subprocess +from threading import Thread +from log import single_line_delim +from framework import VppTestCase, running_extended_tests, VppTestRunner + + +class Worker(Thread): + def __init__(self, args, logger): + self.logger = logger + self.args = args + self.result = None + super(Worker, self).__init__() + + def run(self): + executable = self.args[0] + self.logger.debug("Running executable w/args `%s'" % self.args) + env = os.environ.copy() + env["CK_LOG_FILE_NAME"] = "-" + self.process = subprocess.Popen( + self.args, shell=False, env=env, preexec_fn=os.setpgrp, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = self.process.communicate() + self.logger.debug("Finished running `%s'" % executable) + self.logger.info("Return code is `%s'" % self.process.returncode) + self.logger.info(single_line_delim) + self.logger.info("Executable `%s' wrote to stdout:" % executable) + self.logger.info(single_line_delim) + self.logger.info(out) + self.logger.info(single_line_delim) + self.logger.info("Executable `%s' wrote to stderr:" % executable) + self.logger.info(single_line_delim) + self.logger.error(err) + self.logger.info(single_line_delim) + self.result = self.process.returncode + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class VAPITestCase(VppTestCase): + """ VAPI test """ + + def test_vapi(self): + """ run VAPI tests """ + var = "BR" + built_root = os.getenv(var, None) + self.assertIsNotNone(built_root, + "Environment variable `%s' not set" % var) + executable = "%s/vapi_test/vapi_test" % built_root + worker = Worker( + [executable, "vapi client", self.shm_prefix], self.logger) + worker.start() + timeout = 45 + worker.join(timeout) + self.logger.info("Worker result is `%s'" % worker.result) + error = False + if worker.result is None: + try: + error = True + self.logger.error( + "Timeout! Worker did not finish in %ss" % timeout) + os.killpg(os.getpgid(worker.process.pid), signal.SIGTERM) + worker.join() + except: + raise Exception("Couldn't kill worker-spawned process") + if error: + raise Exception( + "Timeout! Worker did not finish in %ss" % timeout) + self.assert_equal(worker.result, 0, "Binary test return code") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) -- cgit 1.2.3-korg From a61d1251985f236d0a96b3136d6a948fe8fc47fd Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Wed, 27 Sep 2017 06:48:44 +0200 Subject: make test: clean ext binaries when doing test-wipe Change-Id: I9f5212ee670ea91c6b35f1406c256d0687b9c6b5 Signed-off-by: Klement Sekera --- test/Makefile | 1 + 1 file changed, 1 insertion(+) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 132ebee6..721ec963 100644 --- a/test/Makefile +++ b/test/Makefile @@ -140,6 +140,7 @@ reset: @mkdir $(VPP_TEST_FAILED_DIR) wipe: reset + @make -C ext clean @rm -rf $(PYTHON_VENV_PATH) @rm -f $(PAPI_INSTALL_FLAGS) -- cgit 1.2.3-korg From e691345d7a888ad57848c86b86211192e07d5de7 Mon Sep 17 00:00:00 2001 From: Dave Wallace Date: Sat, 30 Sep 2017 01:53:26 -0400 Subject: make test: archive failed test data with build logs. (VPP-1011) - Fix invocation of compress_failed.sh - Fix compress_failed to copy compressed results files to $WORKSPACE/archives and return failure exit code. Failed test case data will be copied to logs.fd.io and found in the archives/-FAILED directory in the build log link in the vpp-verify-master-ubuntu1604 jenkins job page. For example: https://logs.fd.io/production/vex-yul-rot-jenkins-1/vpp-verify-master-ubuntu1604/7353/archives/ Change-Id: Ife9a0737115e69c0a8441e3bb0133af1528d909b Signed-off-by: Dave Wallace (cherry picked from commit 25dc16715ee3fc0a600e2f58841173249bfae501) --- test/Makefile | 3 +-- test/scripts/compress_failed.sh | 46 +++++++++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 17 deletions(-) (limited to 'test/Makefile') diff --git a/test/Makefile b/test/Makefile index 721ec963..da77accc 100644 --- a/test/Makefile +++ b/test/Makefile @@ -86,8 +86,7 @@ $(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) @touch $@ define retest-func - @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) - @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/compress_failed.sh + @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) || env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) COMPRESS_FAILED_TEST_LOGS=$(COMPRESS_FAILED_TEST_LOGS) scripts/compress_failed.sh endef .PHONY: sanity diff --git a/test/scripts/compress_failed.sh b/test/scripts/compress_failed.sh index 6076b3b3..9559e2ac 100755 --- a/test/scripts/compress_failed.sh +++ b/test/scripts/compress_failed.sh @@ -2,20 +2,36 @@ if [ "$(ls -A ${VPP_TEST_FAILED_DIR})" ] then - if [ "${COMPRESS_FAILED_TEST_LOGS}" == "yes" ] - then - echo -n "Compressing files in temporary directories from failed test runs..." - cd ${VPP_TEST_FAILED_DIR} - for d in * - do - cd ${d} - find . ! -path . -print0 | xargs -0 -n1 gzip - cd ${VPP_TEST_FAILED_DIR} - done - echo "done." - else - echo "Not compressing files in temporary directories from failed test runs." - fi + if [ "${COMPRESS_FAILED_TEST_LOGS}" == "yes" ] + then + echo -n "Compressing files in temporary directories from failed test runs... " + cd ${VPP_TEST_FAILED_DIR} + for d in * + do + cd ${d} + find . ! -path . -print0 | xargs -0 -n1 gzip + cd ${VPP_TEST_FAILED_DIR} + done + echo "done." + if [ -n "$WORKSPACE" ] + then + echo "Copying failed test logs into build log archive directory ($WORKSPACE/archives)... " + for failed_test in $(ls $VPP_TEST_FAILED_DIR) + do + mkdir -p $WORKSPACE/archives/$failed_test + cp -a $VPP_TEST_FAILED_DIR/$failed_test/* $WORKSPACE/archives/$failed_test + done + echo "done." + fi + + else + echo "Not compressing files in temporary directories from failed test runs." + fi else - echo "No symlinks to failed tests' temporary directories found in ${VPP_TEST_FAILED_DIR}." + echo "No symlinks to failed tests' temporary directories found in ${VPP_TEST_FAILED_DIR}." fi + +# This script gets run only if there was a 'make test' failure, +# so return failure error status so that the build results are +# recorded correctly. +exit 1 -- cgit 1.2.3-korg