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 --- test/hook.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 test/hook.py (limited to 'test/hook.py') 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() -- 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/hook.py') 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 01bbbe91faac7f70abab389c56bb016af086073f Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Wed, 2 Nov 2016 09:25:05 +0100 Subject: Improve debug-cli in test framework Opening debug-cli only makes sense if there is time for user to use it, so either the run must be debugged or stepped. Only open the debug-cli in these cases. Change-Id: Ied276071797a549880d730cda43c59230a412efe Signed-off-by: Klement Sekera --- test/framework.py | 6 ++++-- test/hook.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'test/hook.py') diff --git a/test/framework.py b/test/framework.py index 8c39701b..ed71908c 100644 --- a/test/framework.py +++ b/test/framework.py @@ -113,8 +113,10 @@ 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.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", - "cli-listen localhost:5002", "}", + 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, "}", "api-segment", "{", "prefix", cls.shm_prefix, "}"] if cls.plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path]) diff --git a/test/hook.py b/test/hook.py index 3ae14737..f81e5187 100644 --- a/test/hook.py +++ b/test/hook.py @@ -73,7 +73,7 @@ class PollHook(Hook): raise Exception("GDB refused to die...") def on_crash(self, core_path): - if self.testcase.interactive: + 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 -- cgit 1.2.3-korg From 085f5c00661ef00a038d3d54be7be6dafd0a6d89 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 24 Nov 2016 01:59:16 +0100 Subject: make test: detect early vpp crash If VPP process dies right at start, do a quick detection instead of being stuck in the connect timeout (60s). Change-Id: I41675181635fb81a6a7d93fbf652480a16bf78a0 Signed-off-by: Klement Sekera --- test/framework.py | 31 +++++++++++++++++-------------- test/hook.py | 3 +-- 2 files changed, 18 insertions(+), 16 deletions(-) (limited to 'test/hook.py') diff --git a/test/framework.py b/test/framework.py index 1375f076..315556a8 100644 --- a/test/framework.py +++ b/test/framework.py @@ -177,6 +177,7 @@ class VppTestCase(unittest.TestCase): cls.pg_streams = [] cls.packet_infos = {} cls.verbose = 0 + cls.vpp_dead = False print(double_line_delim) print(colorize(getdoc(cls).splitlines()[0], YELLOW)) print(double_line_delim) @@ -184,13 +185,22 @@ class VppTestCase(unittest.TestCase): # doesn't get called and we might end with a zombie vpp try: cls.run_vpp() - cls.vpp_dead = False + 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() cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix) if cls.step: - cls.vapi.register_hook(StepHook(cls)) + hook = StepHook(cls) else: - cls.vapi.register_hook(PollHook(cls)) + hook = PollHook(cls) + cls.vapi.register_hook(hook) time.sleep(0.1) + hook.poll_vpp() try: cls.vapi.connect() except: @@ -199,18 +209,11 @@ class VppTestCase(unittest.TestCase): "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: - if hasattr(cls, 'vpp'): - cls.vpp.terminate() - del cls.vpp + try: + cls.quit() + except: + pass raise @classmethod diff --git a/test/hook.py b/test/hook.py index f81e5187..90e9bbf5 100644 --- a/test/hook.py +++ b/test/hook.py @@ -57,7 +57,6 @@ class PollHook(Hook): """ Hook which checks if the vpp subprocess is alive """ def __init__(self, testcase): - self.vpp_dead = False self.testcase = testcase self.logger = testcase.logger @@ -92,7 +91,7 @@ class PollHook(Hook): 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: + if self.testcase.vpp_dead: # already dead, nothing to do return -- cgit 1.2.3-korg From 0e3c0de1ed87f3cdf16e26e05e39ea6eebeafb18 Mon Sep 17 00:00:00 2001 From: Klement Sekera Date: Thu, 29 Sep 2016 14:43:44 +0200 Subject: BFD: basic asynchronous session up/down This is a work-in-progress basic BFD session handling. Only asynchronous mode is supported at the moment. Setting the session flags doesn't work. Change-Id: Idba27f721b5c35be5a66a6d202a63d23ff7ecf6f Signed-off-by: Klement Sekera --- Makefile | 2 + test/bfd.py | 216 ++++++++ test/framework.py | 28 ++ test/hook.py | 20 +- test/template_bd.py | 3 +- test/test_bfd.py | 273 +++++++++++ test/util.py | 22 + test/vpp_object.py | 79 +++ test/vpp_papi_provider.py | 435 +++++++++------- test/vpp_pg_interface.py | 40 +- vlib-api/vlibapi/api_helper_macros.h | 3 +- vnet/Makefile.am | 16 + vnet/vnet/api_errno.h | 4 +- vnet/vnet/bfd/bfd_api.h | 46 ++ vnet/vnet/bfd/bfd_debug.h | 80 +++ vnet/vnet/bfd/bfd_doc.md | 1 + vnet/vnet/bfd/bfd_main.c | 928 +++++++++++++++++++++++++++++++++++ vnet/vnet/bfd/bfd_main.h | 218 ++++++++ vnet/vnet/bfd/bfd_protocol.c | 74 +++ vnet/vnet/bfd/bfd_protocol.h | 154 ++++++ vnet/vnet/bfd/bfd_udp.c | 610 +++++++++++++++++++++++ vnet/vnet/bfd/bfd_udp.h | 56 +++ vnet/vnet/bfd/dir.dox | 18 + vnet/vnet/buffer.h | 4 + vnet/vnet/ip/ip4_forward.c | 12 +- vnet/vnet/ip/udp.h | 2 + vpp/vpp-api/api.c | 168 ++++++- vpp/vpp-api/vpe.api | 185 +++++++ vppinfra/vppinfra/mhash.c | 4 +- vppinfra/vppinfra/mhash.h | 4 +- 30 files changed, 3504 insertions(+), 201 deletions(-) create mode 100644 test/bfd.py create mode 100644 test/test_bfd.py create mode 100644 test/vpp_object.py create mode 100644 vnet/vnet/bfd/bfd_api.h create mode 100644 vnet/vnet/bfd/bfd_debug.h create mode 100644 vnet/vnet/bfd/bfd_doc.md create mode 100644 vnet/vnet/bfd/bfd_main.c create mode 100644 vnet/vnet/bfd/bfd_main.h create mode 100644 vnet/vnet/bfd/bfd_protocol.c create mode 100644 vnet/vnet/bfd/bfd_protocol.h create mode 100644 vnet/vnet/bfd/bfd_udp.c create mode 100644 vnet/vnet/bfd/bfd_udp.h create mode 100644 vnet/vnet/bfd/dir.dox (limited to 'test/hook.py') diff --git a/Makefile b/Makefile index d535389a..00699a76 100644 --- a/Makefile +++ b/Makefile @@ -214,6 +214,8 @@ build-vpp-api: $(BR)/.bootstrap.ok VPP_PYTHON_PREFIX=$(BR)/python + + #$(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-install ,) define test $(if $(filter-out $(3),retest),make -C $(BR) PLATFORM=$(1) TAG=$(2) vpp-api-install plugins-install vpp-install,) make -C test \ diff --git a/test/bfd.py b/test/bfd.py new file mode 100644 index 00000000..beacd80f --- /dev/null +++ b/test/bfd.py @@ -0,0 +1,216 @@ +from socket import AF_INET, AF_INET6 +from scapy.all import * +from scapy.packet import * +from scapy.fields import * +from framework import * +from vpp_object import * +from util import NumericConstant + + +class BFDDiagCode(NumericConstant): + """ BFD Diagnostic Code """ + no_diagnostic = 0 + control_detection_time_expired = 1 + echo_function_failed = 2 + neighbor_signaled_session_down = 3 + forwarding_plane_reset = 4 + path_down = 5 + concatenated_path_down = 6 + administratively_down = 7 + reverse_concatenated_path_down = 8 + + desc_dict = { + no_diagnostic: "No diagnostic", + control_detection_time_expired: "Control Detection Time Expired", + echo_function_failed: "Echo Function Failed", + neighbor_signaled_session_down: "Neighbor Signaled Session Down", + forwarding_plane_reset: "Forwarding Plane Reset", + path_down: "Path Down", + concatenated_path_down: "Concatenated Path Down", + administratively_down: "Administratively Down", + reverse_concatenated_path_down: "Reverse Concatenated Path Down", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFDState(NumericConstant): + """ BFD State """ + admin_down = 0 + down = 1 + init = 2 + up = 3 + + desc_dict = { + admin_down: "AdminDown", + down: "Down", + init: "Init", + up: "Up", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFD(Packet): + + udp_dport = 3784 #: BFD destination port per RFC 5881 + udp_sport_min = 49152 #: BFD source port min value per RFC 5881 + udp_sport_max = 65535 #: BFD source port max value per RFC 5881 + + name = "BFD" + + fields_desc = [ + BitField("version", 1, 3), + BitEnumField("diag", 0, 5, BFDDiagCode.desc_dict), + BitEnumField("state", 0, 2, BFDState.desc_dict), + FlagsField("flags", 0, 6, ['P', 'F', 'C', 'A', 'D', 'M']), + XByteField("detect_mult", 0), + XByteField("length", 24), + BitField("my_discriminator", 0, 32), + BitField("your_discriminator", 0, 32), + BitField("desired_min_tx_interval", 0, 32), + BitField("required_min_rx_interval", 0, 32), + BitField("required_min_echo_rx_interval", 0, 32)] + + def mysummary(self): + return self.sprintf("BFD(my_disc=%BFD.my_discriminator%," + "your_disc=%BFD.your_discriminator%)") + +# glue the BFD packet class to scapy parser +bind_layers(UDP, BFD, dport=BFD.udp_dport) + + +class VppBFDUDPSession(VppObject): + """ Represents BFD UDP session in VPP """ + + @property + def test(self): + """ Test which created this session """ + return self._test + + @property + def interface(self): + """ Interface on which this session lives """ + return self._interface + + @property + def af(self): + """ Address family - AF_INET or AF_INET6 """ + return self._af + + @property + def bs_index(self): + """ BFD session index from VPP """ + if self._bs_index is not None: + return self._bs_index + raise NotConfiguredException("not configured") + + @property + def local_addr(self): + """ BFD session local address (VPP address) """ + if self._local_addr is None: + return self._interface.local_ip4 + return self._local_addr + + @property + def local_addr_n(self): + """ BFD session local address (VPP address) - raw, suitable for API """ + if self._local_addr is None: + return self._interface.local_ip4n + return self._local_addr_n + + @property + def peer_addr(self): + """ BFD session peer address """ + return self._peer_addr + + @property + def peer_addr_n(self): + """ BFD session peer address - raw, suitable for API """ + return self._peer_addr_n + + @property + def state(self): + """ BFD session state """ + result = self.test.vapi.bfd_udp_session_dump() + session = None + for s in result: + if s.sw_if_index == self.interface.sw_if_index: + if self.af == AF_INET \ + and s.is_ipv6 == 0 \ + and self.interface.local_ip4n == s.local_addr[:4] \ + and self.interface.remote_ip4n == s.peer_addr[:4]: + session = s + break + if session is None: + raise Exception( + "Could not find BFD session in VPP response: %s" % repr(result)) + return session.state + + @property + def desired_min_tx(self): + return self._desired_min_tx + + @property + def required_min_rx(self): + return self._required_min_rx + + @property + def detect_mult(self): + return self._detect_mult + + def __init__(self, test, interface, peer_addr, local_addr=None, af=AF_INET): + self._test = test + self._interface = interface + self._af = af + self._local_addr = local_addr + self._peer_addr = peer_addr + self._peer_addr_n = socket.inet_pton(af, peer_addr) + self._bs_index = None + self._desired_min_tx = 200000 # 0.2s + self._required_min_rx = 200000 # 0.2s + self._detect_mult = 3 # 3 packets need to be missed + + def add_vpp_config(self): + is_ipv6 = 1 if AF_INET6 == self.af else 0 + result = self.test.vapi.bfd_udp_add( + self._interface.sw_if_index, + self.desired_min_tx, + self.required_min_rx, + self.detect_mult, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + self._bs_index = result.bs_index + + def query_vpp_config(self): + result = self.test.vapi.bfd_udp_session_dump() + session = None + for s in result: + if s.sw_if_index == self.interface.sw_if_index: + if self.af == AF_INET \ + and s.is_ipv6 == 0 \ + and self.interface.local_ip4n == s.local_addr[:4] \ + and self.interface.remote_ip4n == s.peer_addr[:4]: + session = s + break + if session is None: + return False + return True + + def remove_vpp_config(self): + if hasattr(self, '_bs_index'): + is_ipv6 = 1 if AF_INET6 == self._af else 0 + self.test.vapi.bfd_udp_del( + self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + + def object_id(self): + return "bfd-udp-%d" % self.bs_index + + def admin_up(self): + self.test.vapi.bfd_session_set_flags(self.bs_index, 1) diff --git a/test/framework.py b/test/framework.py index b3cbb08a..aa4f2fdf 100644 --- a/test/framework.py +++ b/test/framework.py @@ -462,6 +462,34 @@ class VppTestCase(unittest.TestCase): if info.dst == dst_index: return info + def assert_equal(self, real_value, expected_value, name_or_class=None): + if name_or_class is None: + self.assertEqual(real_value, expected_value, msg) + return + try: + msg = "Invalid %s: %d('%s') does not match expected value %d('%s')" + msg = msg % (getdoc(name_or_class).strip(), + real_value, str(name_or_class(real_value)), + expected_value, str(name_or_class(expected_value))) + except: + msg = "Invalid %s: %s does not match expected value %s" % ( + name_or_class, real_value, expected_value) + + self.assertEqual(real_value, expected_value, msg) + + def assert_in_range( + self, + real_value, + expected_min, + expected_max, + name=None): + if name is None: + msg = None + else: + msg = "Invalid %s: %s out of range <%s,%s>" % ( + name, real_value, expected_min, expected_max) + self.assertTrue(expected_min <= real_value <= expected_max, msg) + class VppTestResult(unittest.TestResult): """ diff --git a/test/hook.py b/test/hook.py index 90e9bbf5..f3e5f880 100644 --- a/test/hook.py +++ b/test/hook.py @@ -100,9 +100,13 @@ class PollHook(Hook): signaldict = dict( (k, v) for v, k in reversed(sorted(signal.__dict__.items())) if v.startswith('SIG') and not v.startswith('SIG_')) + + if self.testcase.vpp.returncode in signaldict: + s = signaldict[abs(self.testcase.vpp.returncode)] + else: + s = "unknown" msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( - self.testcase.vpp.returncode, - signaldict[abs(self.testcase.vpp.returncode)]) + self.testcase.vpp.returncode, s) self.logger.critical(msg) core_path = self.testcase.tempdir + '/core' if os.path.isfile(core_path): @@ -110,27 +114,27 @@ class PollHook(Hook): self.testcase.vpp_dead = True raise VppDiedError(msg) - def after_api(self, api_name, api_args): + def before_api(self, api_name, api_args): """ - Check if VPP died after executing an API + Check if VPP died before executing an API :param api_name: name of the API :param api_args: tuple containing the API arguments :raises VppDiedError: exception if VPP is not running anymore """ - super(PollHook, self).after_api(api_name, api_args) + super(PollHook, self).before_api(api_name, api_args) self.poll_vpp() - def after_cli(self, cli): + def before_cli(self, cli): """ - Check if VPP died after executing a CLI + Check if VPP died before executing a CLI :param cli: CLI string :raises Exception: exception if VPP is not running anymore """ - super(PollHook, self).after_cli(cli) + super(PollHook, self).before_cli(cli) self.poll_vpp() diff --git a/test/template_bd.py b/test/template_bd.py index 6c6fb3da..01e8b855 100644 --- a/test/template_bd.py +++ b/test/template_bd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from abc import abstractmethod +from abc import abstractmethod, ABCMeta from scapy.layers.l2 import Ether, Raw from scapy.layers.inet import IP, UDP @@ -8,6 +8,7 @@ from scapy.layers.inet import IP, UDP class BridgeDomain(object): """ Bridge domain abstraction """ + __metaclass__ = ABCMeta @property def frame_pg0_to_pg1(self): diff --git a/test/test_bfd.py b/test/test_bfd.py new file mode 100644 index 00000000..20d3aea5 --- /dev/null +++ b/test/test_bfd.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python + +import unittest +import time +from random import randint +from bfd import * +from framework import * +from util import ppp + + +class BFDCLITestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) - CLI""" + + @classmethod + def setUpClass(cls): + super(BFDCLITestCase, cls).setUpClass() + + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip4() + cls.pg0.resolve_arp() + + except Exception: + super(BFDCLITestCase, cls).tearDownClass() + raise + + def test_add_bfd(self): + """ create a BFD session """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + self.logger.debug("Session state is %s" % str(session.state)) + session.remove_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + self.logger.debug("Session state is %s" % str(session.state)) + session.remove_vpp_config() + + def test_double_add(self): + """ create the same BFD session twice (negative case) """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + try: + session.add_vpp_config() + except: + session.remove_vpp_config() + return + session.remove_vpp_config() + raise Exception("Expected failure while adding duplicate " + "configuration") + + +def create_packet(interface, ttl=255, src_port=50000, **kwargs): + p = (Ether(src=interface.remote_mac, dst=interface.local_mac) / + IP(src=interface.remote_ip4, dst=interface.local_ip4, ttl=ttl) / + UDP(sport=src_port, dport=BFD.udp_dport) / + BFD(*kwargs)) + return p + + +def verify_ip(test, packet, local_ip, remote_ip): + """ Verify correctness of IP layer. """ + ip = packet[IP] + test.assert_equal(ip.src, local_ip, "IP source address") + test.assert_equal(ip.dst, remote_ip, "IP destination address") + test.assert_equal(ip.ttl, 255, "IP TTL") + + +def verify_udp(test, packet): + """ Verify correctness of UDP layer. """ + udp = packet[UDP] + test.assert_equal(udp.dport, BFD.udp_dport, "UDP destination port") + test.assert_in_range(udp.sport, BFD.udp_sport_min, BFD.udp_sport_max, + "UDP source port") + + +class BFDTestSession(object): + + def __init__(self, test, interface, detect_mult=3): + self.test = test + self.interface = interface + self.bfd_values = { + 'my_discriminator': 0, + 'desired_min_tx_interval': 500000, + 'detect_mult': detect_mult, + 'diag': BFDDiagCode.no_diagnostic, + } + + def update(self, **kwargs): + self.bfd_values.update(kwargs) + + def create_packet(self): + packet = create_packet(self.interface) + for name, value in self.bfd_values.iteritems(): + packet[BFD].setfieldval(name, value) + return packet + + def send_packet(self): + p = self.create_packet() + self.test.logger.debug(ppp("Sending packet:", p)) + self.test.pg0.add_stream([p]) + self.test.pg_start() + + def verify_packet(self, packet): + """ Verify correctness of BFD layer. """ + bfd = packet[BFD] + self.test.assert_equal(bfd.version, 1, "BFD version") + self.test.assert_equal(bfd.your_discriminator, + self.bfd_values['my_discriminator'], + "BFD - your discriminator") + + +class BFDTestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD)""" + + @classmethod + def setUpClass(cls): + super(BFDTestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0, 1]) + cls.pg0.config_ip4() + cls.pg0.generate_remote_hosts() + cls.pg0.configure_ipv4_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFDTestCase, cls).tearDownClass() + raise + + def setUp(self): + self.vapi.want_bfd_events() + self.vpp_session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0) + + def tearDown(self): + self.vapi.want_bfd_events(enable_disable=0) + if not self.vpp_dead: + self.vpp_session.remove_vpp_config() + super(BFDTestCase, self).tearDown() + + def verify_event(self, event, expected_state): + """ Verify correctness of event values. """ + e = event + self.logger.debug("Event: %s" % repr(e)) + self.assert_equal(e.bs_index, self.vpp_session.bs_index, + "BFD session index") + self.assert_equal(e.sw_if_index, self.vpp_session.interface.sw_if_index, + "BFD interface index") + is_ipv6 = 0 + if self.vpp_session.af == AF_INET6: + is_ipv6 = 1 + self.assert_equal(e.is_ipv6, is_ipv6, "is_ipv6") + if self.vpp_session.af == AF_INET: + self.assert_equal(e.local_addr[:4], self.vpp_session.local_addr_n, + "Local IPv4 address") + self.assert_equal(e.peer_addr[:4], self.vpp_session.peer_addr_n, + "Peer IPv4 address") + else: + self.assert_equal(e.local_addr, self.vpp_session.local_addr_n, + "Local IPv6 address") + self.assert_equal(e.peer_addr, self.vpp_session.peer_addr_n, + "Peer IPv6 address") + self.assert_equal(e.state, expected_state, BFDState) + + def wait_for_bfd_packet(self, timeout=1): + p = self.pg0.wait_for_packet(timeout=timeout) + bfd = p[BFD] + if bfd is None: + raise Exception(ppp("Unexpected or invalid BFD packet:", p)) + if bfd.payload: + raise Exception(ppp("Unexpected payload in BFD packet:", bfd)) + verify_ip(self, p, self.pg0.local_ip4, self.pg0.remote_ip4) + verify_udp(self, p) + self.test_session.verify_packet(p) + return p + + def test_slow_timer(self): + """ Slow timer """ + + self.pg_enable_capture([self.pg0]) + expected_packets = 10 + self.logger.info("Waiting for %d BFD packets" % expected_packets) + self.wait_for_bfd_packet() + for i in range(expected_packets): + before = time.time() + self.wait_for_bfd_packet() + after = time.time() + self.assert_in_range( + after - before, 0.75, 1, "time between slow packets") + before = after + + def test_zero_remote_min_rx(self): + """ Zero RemoteMinRxInterval """ + self.pg_enable_capture([self.pg0]) + p = self.wait_for_bfd_packet() + self.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init, + required_min_rx_interval=0) + self.test_session.send_packet() + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.up) + + try: + p = self.pg0.wait_for_packet(timeout=1) + except: + return + raise Exception(ppp("Received unexpected BFD packet:", p)) + + def bfd_conn_up(self): + self.pg_enable_capture([self.pg0]) + self.logger.info("Waiting for slow hello") + p = self.wait_for_bfd_packet() + self.logger.info("Sending Init") + self.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init, + required_min_rx_interval=500000) + self.test_session.send_packet() + self.logger.info("Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.up) + self.logger.info("Session is Up") + self.test_session.update(state=BFDState.up) + + def test_conn_up(self): + """ Basic connection up """ + self.bfd_conn_up() + + def test_hold_up(self): + """ Hold BFD up """ + self.bfd_conn_up() + for i in range(5): + self.wait_for_bfd_packet() + self.test_session.send_packet() + + def test_conn_down(self): + """ Session down after inactivity """ + self.bfd_conn_up() + self.wait_for_bfd_packet() + self.assert_equal( + 0, len(self.vapi.collect_events()), + "number of bfd events") + self.wait_for_bfd_packet() + self.assert_equal( + 0, len(self.vapi.collect_events()), + "number of bfd events") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.verify_event(e, expected_state=BFDState.down) + + @unittest.skip("this test is not working yet") + def test_large_required_min_rx(self): + self.bfd_conn_up() + interval = 5000000 + self.test_session.update(required_min_rx_interval=interval) + self.test_session.send_packet() + now = time.time() + count = 1 + while time.time() < now + interval / 1000000: + try: + p = self.wait_for_bfd_packet() + if count > 1: + self.logger.error(ppp("Received unexpected packet:", p)) + count += 1 + except: + pass + self.assert_equal(count, 1, "number of packets received") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py index 643377f5..f6c6acd4 100644 --- a/test/util.py +++ b/test/util.py @@ -1,5 +1,6 @@ import socket import sys +from abc import abstractmethod, ABCMeta from cStringIO import StringIO @@ -14,6 +15,27 @@ def ppp(headline, packet): return o.getvalue() +class NumericConstant(object): + __metaclass__ = ABCMeta + + desc_dict = {} + + @abstractmethod + def __init__(self, value): + self._value = value + + def __int__(self): + return self._value + + def __long__(self): + return self._value + + def __str__(self): + if self._value in self.desc_dict: + return self.desc_dict[self._value] + return "" + + class Host(object): """ Generic test host "connected" to VPPs interface. """ diff --git a/test/vpp_object.py b/test/vpp_object.py new file mode 100644 index 00000000..2b71fc1f --- /dev/null +++ b/test/vpp_object.py @@ -0,0 +1,79 @@ +from abc import ABCMeta, abstractmethod + + +class VppObject(object): + """ Abstract vpp object """ + __metaclass__ = ABCMeta + + def __init__(self): + VppObjectRegistry().register(self) + + @abstractmethod + def add_vpp_config(self): + """ Add the configuration for this object to vpp. """ + pass + + @abstractmethod + def query_vpp_config(self): + """Query the vpp configuration. + + :return: True if the object is configured""" + pass + + @abstractmethod + def remove_vpp_config(self): + """ Remove the configuration for this object from vpp. """ + pass + + @abstractmethod + def object_id(self): + """ Return a unique string representing this object. """ + pass + + +class VppObjectRegistry(object): + """ Class which handles automatic configuration cleanup. """ + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + if not hasattr(self, "_object_registry"): + self._object_registry = [] + if not hasattr(self, "_object_dict"): + self._object_dict = dict() + + def register(self, o): + """ Register an object in the registry. """ + if not o.unique_id() in self._object_dict: + self._object_registry.append(o) + self._object_dict[o.unique_id()] = o + else: + print "not adding duplicate %s" % o + + def remove_vpp_config(self, logger): + """ + Remove configuration (if present) from vpp and then remove all objects + from the registry. + """ + if not self._object_registry: + logger.info("No objects registered for auto-cleanup.") + return + logger.info("Removing VPP configuration for registered objects") + for o in reversed(self._object_registry): + if o.query_vpp_config(): + logger.info("Removing %s", o) + o.remove_vpp_config() + else: + logger.info("Skipping %s, configuration not present", o) + failed = [] + for o in self._object_registry: + if o.query_vpp_config(): + failed.append(o) + self._object_registry = [] + self._object_dict = dict() + if failed: + logger.error("Couldn't remove configuration for object(s):") + for x in failed: + logger.error(repr(x)) + raise Exception("Couldn't remove configuration for object(s): %s" % + (", ".join(str(x) for x in failed))) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 2f931803..5e80a03e 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,7 +1,12 @@ -import os, fnmatch -import array +import os +import fnmatch +import time from hook import Hook +# Sphinx creates auto-generated documentation by importing the python source +# files and collecting the docstrings from them. The NO_VPP_PAPI flag allows the +# vpp_papi_provider.py file to be importable without having to build the whole +# vpp api if the user only wishes to generate the test documentation. do_import = True try: no_vpp_papi = os.getenv("NO_VPP_PAPI") @@ -17,9 +22,11 @@ if do_import: MPLS_IETF_MAX_LABEL = 0xfffff MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 + class L2_VTR_OP: L2_POP_1 = 3 + class VppPapiProvider(object): """VPP-api provider using vpp-papi @@ -35,12 +42,13 @@ class VppPapiProvider(object): self.test_class = test_class jsonfiles = [] - install_dir=os.getenv('VPP_TEST_INSTALL_PATH') + install_dir = os.getenv('VPP_TEST_INSTALL_PATH') for root, dirnames, filenames in os.walk(install_dir): for filename in fnmatch.filter(filenames, '*.api.json'): jsonfiles.append(os.path.join(root, filename)) self.papi = VPP(jsonfiles) + self._events = list() def register_hook(self, hook): """Replace hook registration with new hook @@ -50,9 +58,35 @@ class VppPapiProvider(object): """ self.hook = hook + def collect_events(self): + e = self._events + self._events = list() + return e + + def wait_for_event(self, timeout, name=None): + limit = time.time() + timeout + while time.time() < limit: + if self._events: + e = self._events.pop(0) + if name and type(e).__name__ != name: + raise Exception( + "Unexpected event received: %s, expected: %s" % + (type(e).__name__, name)) + return e + time.sleep(0) # yield + if name is not None: + raise Exception("Event %s did not occur within timeout" % name) + raise Exception("Event did not occur within timeout") + + def __call__(self, name, event): + # FIXME use the name instead of relying on type(e).__name__ ? + # FIXME #2 if this throws, it is eaten silently, Ole? + self._events.append(event) + def connect(self): """Connect the API to VPP""" self.papi.connect(self.name, self.shm_prefix) + self.papi.register_event_callback(self) def disconnect(self): """Disconnect the API from VPP""" @@ -73,7 +107,7 @@ class VppPapiProvider(object): if hasattr(reply, 'retval') and reply.retval != expected_retval: msg = "API call failed, expected retval == %d, got %s" % ( expected_retval, repr(reply)) - self.test_class.test_instance.logger.error(msg) + self.test_class.logger.error(msg) raise Exception(msg) self.hook.after_api(api_fn.__name__, api_args) return reply @@ -116,7 +150,7 @@ class VppPapiProvider(object): """ return self.api(self.papi.pg_create_interface, - { "interface_id" : pg_index }) + {"interface_id": pg_index}) def sw_interface_dump(self, filter=None): """ @@ -125,7 +159,7 @@ class VppPapiProvider(object): """ if filter is not None: - args = {"name_filter_valid" : 1, "name_filter" : filter} + args = {"name_filter_valid": 1, "name_filter": filter} else: args = {} return self.api(self.papi.sw_interface_dump, args) @@ -140,8 +174,8 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_table, - { 'sw_if_index' : sw_if_index, 'is_ipv6' : is_ipv6, - 'vrf_id' : table_id}) + {'sw_if_index': sw_if_index, 'is_ipv6': is_ipv6, + 'vrf_id': table_id}) def sw_interface_add_del_address(self, sw_if_index, addr, addr_len, is_ipv6=0, is_add=1, del_all=0): @@ -156,12 +190,12 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_add_del_address, - { 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'del_all' : del_all, - 'address_length' : addr_len, - 'address' : addr}) + {'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'del_all': del_all, + 'address_length': addr_len, + 'address': addr}) def sw_interface_enable_disable_mpls(self, sw_if_index, is_enable=1): @@ -172,12 +206,12 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_mpls_enable, - {'sw_if_index' : sw_if_index, - 'enable' : is_enable }) + {'sw_if_index': sw_if_index, + 'enable': is_enable}) def sw_interface_ra_suppress(self, sw_if_index): return self.api(self.papi.sw_interface_ip6nd_ra_config, - {'sw_if_index' : sw_if_index }) + {'sw_if_index': sw_if_index}) def vxlan_add_del_tunnel( self, @@ -202,14 +236,14 @@ class VppPapiProvider(object): """ return self.api(self.papi.vxlan_add_del_tunnel, - {'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'src_address' : src_addr, - 'dst_address' : dst_addr, - 'mcast_sw_if_index' : mcast_sw_if_index, - 'encap_vrf_id' : encap_vrf_id, - 'decap_next_index' : decap_next_index, - 'vni' : vni}) + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'src_address': src_addr, + 'dst_address': dst_addr, + 'mcast_sw_if_index': mcast_sw_if_index, + 'encap_vrf_id': encap_vrf_id, + 'decap_next_index': decap_next_index, + 'vni': vni}) def bridge_domain_add_del(self, bd_id, flood=1, uu_flood=1, forward=1, learn=1, arp_term=0, is_add=1): @@ -229,13 +263,13 @@ class VppPapiProvider(object): :param int is_add: Add or delete flag. (Default value = 1) """ return self.api(self.papi.bridge_domain_add_del, - { 'bd_id' : bd_id, - 'flood' : flood, - 'uu_flood' : uu_flood, - 'forward' : forward, - 'learn' : learn, - 'arp_term' : arp_term, - 'is_add' : is_add}) + {'bd_id': bd_id, + 'flood': flood, + 'uu_flood': uu_flood, + 'forward': forward, + 'learn': learn, + 'arp_term': arp_term, + 'is_add': is_add}) def l2fib_add_del(self, mac, bd_id, sw_if_index, is_add=1, static_mac=0, filter_mac=0, bvi_mac=0): @@ -254,13 +288,13 @@ class VppPapiProvider(object): interface. (Default value = 0) """ return self.api(self.papi.l2fib_add_del, - { 'mac' : self._convert_mac(mac), - 'bd_id' : bd_id, - 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'static_mac' : static_mac, - 'filter_mac' : filter_mac, - 'bvi_mac' : bvi_mac }) + {'mac': self._convert_mac(mac), + 'bd_id': bd_id, + 'sw_if_index': sw_if_index, + 'is_add': is_add, + 'static_mac': static_mac, + 'filter_mac': filter_mac, + 'bvi_mac': bvi_mac}) def sw_interface_set_l2_bridge(self, sw_if_index, bd_id, shg=0, bvi=0, enable=1): @@ -274,11 +308,11 @@ class VppPapiProvider(object): :param int enable: Add or remove interface. (Default value = 1) """ return self.api(self.papi.sw_interface_set_l2_bridge, - { 'rx_sw_if_index' : sw_if_index, - 'bd_id' : bd_id, - 'shg' : shg, - 'bvi' : bvi, - 'enable' : enable }) + {'rx_sw_if_index': sw_if_index, + 'bd_id': bd_id, + 'shg': shg, + 'bvi': bvi, + 'enable': enable}) def bridge_flags(self, bd_id, is_set, feature_bitmap): """Enable/disable required feature of the bridge domain with defined ID. @@ -293,9 +327,9 @@ class VppPapiProvider(object): - arp-term (1 << 4). """ return self.api(self.papi.bridge_flags, - {'bd_id' : bd_id, - 'is_set' : is_set, - 'feature_bitmap' : feature_bitmap }) + {'bd_id': bd_id, + 'is_set': is_set, + 'feature_bitmap': feature_bitmap}) def bridge_domain_dump(self, bd_id=0): """ @@ -305,7 +339,7 @@ class VppPapiProvider(object): :return: Dictionary of bridge domain(s) data. """ return self.api(self.papi.bridge_domain_dump, - {'bd_id' : bd_id }) + {'bd_id': bd_id}) def sw_interface_set_l2_xconnect(self, rx_sw_if_index, tx_sw_if_index, enable): @@ -319,11 +353,17 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_l2_xconnect, - { 'rx_sw_if_index' : rx_sw_if_index, - 'tx_sw_if_index' : tx_sw_if_index, - 'enable' : enable }) + {'rx_sw_if_index': rx_sw_if_index, + 'tx_sw_if_index': tx_sw_if_index, + 'enable': enable}) - def sw_interface_set_l2_tag_rewrite(self, sw_if_index, vtr_oper, push=0, tag1=0, tag2=0): + def sw_interface_set_l2_tag_rewrite( + self, + sw_if_index, + vtr_oper, + push=0, + tag1=0, + tag2=0): """L2 interface vlan tag rewrite configure request :param client_index - opaque cookie to identify the sender :param context - sender context, to match reply w/ request @@ -335,11 +375,11 @@ class VppPapiProvider(object): """ return self.api(self.papi.l2_interface_vlan_tag_rewrite, - { 'sw_if_index' : sw_if_index, - 'vtr_op' : vtr_oper, - 'push_dot1q' : push, - 'tag1' : tag1, - 'tag2' : tag2 }) + {'sw_if_index': sw_if_index, + 'vtr_op': vtr_oper, + 'push_dot1q': push, + 'tag1': tag1, + 'tag2': tag2}) def sw_interface_set_flags(self, sw_if_index, admin_up_down, link_up_down=0, deleted=0): @@ -352,10 +392,10 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_set_flags, - { 'sw_if_index' : sw_if_index, - 'admin_up_down' : admin_up_down, - 'link_up_down' : link_up_down, - 'deleted' : deleted }) + {'sw_if_index': sw_if_index, + 'admin_up_down': admin_up_down, + 'link_up_down': link_up_down, + 'deleted': deleted}) def create_subif(self, sw_if_index, sub_id, outer_vlan, inner_vlan, no_tags=0, one_tag=0, two_tags=0, dot1ad=0, exact_match=0, @@ -379,18 +419,18 @@ class VppPapiProvider(object): """ return self.api( self.papi.create_subif, - { 'sw_if_index' : sw_if_index, - 'sub_id' : sub_id, - 'no_tags' : no_tags, - 'one_tag' : one_tag, - 'two_tags' : two_tags, - 'dot1ad' : dot1ad, - 'exact_match' : exact_match, - 'default_sub' : default_sub, - 'outer_vlan_id_any' : outer_vlan_id_any, - 'inner_vlan_id_any' : inner_vlan_id_any, - 'outer_vlan_id' : outer_vlan, - 'inner_vlan_id' : inner_vlan }) + {'sw_if_index': sw_if_index, + 'sub_id': sub_id, + 'no_tags': no_tags, + 'one_tag': one_tag, + 'two_tags': two_tags, + 'dot1ad': dot1ad, + 'exact_match': exact_match, + 'default_sub': default_sub, + 'outer_vlan_id_any': outer_vlan_id_any, + 'inner_vlan_id_any': inner_vlan_id_any, + 'outer_vlan_id': outer_vlan, + 'inner_vlan_id': inner_vlan}) def delete_subif(self, sw_if_index): """Delete subinterface @@ -398,7 +438,7 @@ class VppPapiProvider(object): :param sw_if_index: """ return self.api(self.papi.delete_subif, - { 'sw_if_index' : sw_if_index }) + {'sw_if_index': sw_if_index}) def create_vlan_subif(self, sw_if_index, vlan): """ @@ -408,8 +448,8 @@ class VppPapiProvider(object): """ return self.api(self.papi.create_vlan_subif, - {'sw_if_index' : sw_if_index, - 'vlan_id' : vlan }) + {'sw_if_index': sw_if_index, + 'vlan_id': vlan}) def create_loopback(self, mac=''): """ @@ -417,7 +457,7 @@ class VppPapiProvider(object): :param mac: (Optional) """ return self.api(self.papi.create_loopback, - { 'mac_address' : mac }) + {'mac_address': mac}) def ip_add_del_route( self, @@ -428,9 +468,9 @@ class VppPapiProvider(object): table_id=0, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_resolve_host=0, is_resolve_attached=0, @@ -470,29 +510,29 @@ class VppPapiProvider(object): return self.api( self.papi.ip_add_del_route, - { 'next_hop_sw_if_index' : next_hop_sw_if_index, - 'table_id' : table_id, - 'classify_table_index' : classify_table_index, - 'next_hop_table_id' : next_hop_table_id, - 'create_vrf_if_needed' : create_vrf_if_needed, - 'is_add' : is_add, - 'is_drop' : is_drop, - 'is_unreach' : is_unreach, - 'is_prohibit' : is_prohibit, - 'is_ipv6' : is_ipv6, - 'is_local' : is_local, - 'is_classify' : is_classify, - 'is_multipath' : is_multipath, - 'is_resolve_host' : is_resolve_host, - 'is_resolve_attached' : is_resolve_attached, - 'not_last' : not_last, - 'next_hop_weight' : next_hop_weight, - 'dst_address_length' : dst_address_length, - 'dst_address' : dst_address, - 'next_hop_address' : next_hop_address, - 'next_hop_n_out_labels' : next_hop_n_out_labels, - 'next_hop_via_label' : next_hop_via_label, - 'next_hop_out_label_stack' : next_hop_out_label_stack }) + {'next_hop_sw_if_index': next_hop_sw_if_index, + 'table_id': table_id, + 'classify_table_index': classify_table_index, + 'next_hop_table_id': next_hop_table_id, + 'create_vrf_if_needed': create_vrf_if_needed, + 'is_add': is_add, + 'is_drop': is_drop, + 'is_unreach': is_unreach, + 'is_prohibit': is_prohibit, + 'is_ipv6': is_ipv6, + 'is_local': is_local, + 'is_classify': is_classify, + 'is_multipath': is_multipath, + 'is_resolve_host': is_resolve_host, + 'is_resolve_attached': is_resolve_attached, + 'not_last': not_last, + 'next_hop_weight': next_hop_weight, + 'dst_address_length': dst_address_length, + 'dst_address': dst_address, + 'next_hop_address': next_hop_address, + 'next_hop_n_out_labels': next_hop_n_out_labels, + 'next_hop_via_label': next_hop_via_label, + 'next_hop_out_label_stack': next_hop_out_label_stack}) def ip_neighbor_add_del(self, sw_if_index, @@ -516,13 +556,13 @@ class VppPapiProvider(object): return self.api( self.papi.ip_neighbor_add_del, - { 'vrf_id' : vrf_id, - 'sw_if_index' : sw_if_index, - 'is_add' : is_add, - 'is_ipv6' : is_ipv6, - 'is_static' : is_static, - 'mac_address' : mac_address, - 'dst_address' : dst_address + {'vrf_id': vrf_id, + 'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'is_static': is_static, + 'mac_address': mac_address, + 'dst_address': dst_address } ) @@ -536,9 +576,9 @@ class VppPapiProvider(object): """ return self.api(self.papi.sw_interface_span_enable_disable, - { 'sw_if_index_from' : sw_if_index_from, - 'sw_if_index_to' : sw_if_index_to, - 'state' : state }) + {'sw_if_index_from': sw_if_index_from, + 'sw_if_index_to': sw_if_index_to, + 'state': state}) def gre_tunnel_add_del(self, src_address, @@ -559,12 +599,12 @@ class VppPapiProvider(object): return self.api( self.papi.gre_add_del_tunnel, - { 'is_add' : is_add, - 'is_ipv6' : is_ip6, - 'teb' : is_teb, - 'src_address' : src_address, - 'dst_address' : dst_address, - 'outer_fib_id' : outer_fib_id } + {'is_add': is_add, + 'is_ipv6': is_ip6, + 'teb': is_teb, + 'src_address': src_address, + 'dst_address': dst_address, + 'outer_fib_id': outer_fib_id} ) def mpls_route_add_del( @@ -577,9 +617,9 @@ class VppPapiProvider(object): table_id=0, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_resolve_host=0, is_resolve_attached=0, @@ -615,24 +655,24 @@ class VppPapiProvider(object): return self.api( self.papi.mpls_route_add_del, - { 'mr_label' : label, - 'mr_eos' : eos, - 'mr_table_id' : table_id, - 'mr_classify_table_index' : classify_table_index, - 'mr_create_table_if_needed' : create_vrf_if_needed, - 'mr_is_add' : is_add, - 'mr_is_classify' : is_classify, - 'mr_is_multipath' : is_multipath, - 'mr_is_resolve_host' : is_resolve_host, - 'mr_is_resolve_attached' : is_resolve_attached, - 'mr_next_hop_proto_is_ip4' : next_hop_proto_is_ip4, - 'mr_next_hop_weight' : next_hop_weight, - 'mr_next_hop' : next_hop_address, - 'mr_next_hop_n_out_labels' : next_hop_n_out_labels, - 'mr_next_hop_sw_if_index' : next_hop_sw_if_index, - 'mr_next_hop_table_id' : next_hop_table_id, - 'mr_next_hop_via_label' : next_hop_via_label, - 'mr_next_hop_out_label_stack' : next_hop_out_label_stack }) + {'mr_label': label, + 'mr_eos': eos, + 'mr_table_id': table_id, + 'mr_classify_table_index': classify_table_index, + 'mr_create_table_if_needed': create_vrf_if_needed, + 'mr_is_add': is_add, + 'mr_is_classify': is_classify, + 'mr_is_multipath': is_multipath, + 'mr_is_resolve_host': is_resolve_host, + 'mr_is_resolve_attached': is_resolve_attached, + 'mr_next_hop_proto_is_ip4': next_hop_proto_is_ip4, + 'mr_next_hop_weight': next_hop_weight, + 'mr_next_hop': next_hop_address, + 'mr_next_hop_n_out_labels': next_hop_n_out_labels, + 'mr_next_hop_sw_if_index': next_hop_sw_if_index, + 'mr_next_hop_table_id': next_hop_table_id, + 'mr_next_hop_via_label': next_hop_via_label, + 'mr_next_hop_out_label_stack': next_hop_out_label_stack}) def mpls_ip_bind_unbind( self, @@ -648,14 +688,14 @@ class VppPapiProvider(object): """ return self.api( self.papi.mpls_ip_bind_unbind, - {'mb_mpls_table_id' : table_id, - 'mb_label' : label, - 'mb_ip_table_id' : ip_table_id, - 'mb_create_table_if_needed' : create_vrf_if_needed, - 'mb_is_bind' : is_bind, - 'mb_is_ip4' : is_ip4, - 'mb_address_length' : dst_address_length, - 'mb_address' : dst_address}) + {'mb_mpls_table_id': table_id, + 'mb_label': label, + 'mb_ip_table_id': ip_table_id, + 'mb_create_table_if_needed': create_vrf_if_needed, + 'mb_is_bind': is_bind, + 'mb_is_ip4': is_ip4, + 'mb_address_length': dst_address_length, + 'mb_address': dst_address}) def mpls_tunnel_add_del( self, @@ -665,9 +705,9 @@ class VppPapiProvider(object): next_hop_sw_if_index=0xFFFFFFFF, next_hop_table_id=0, next_hop_weight=1, - next_hop_n_out_labels = 0, - next_hop_out_label_stack = [], - next_hop_via_label = MPLS_LABEL_INVALID, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, create_vrf_if_needed=0, is_add=1, l2_only=0): @@ -696,19 +736,16 @@ class VppPapiProvider(object): """ return self.api( self.papi.mpls_tunnel_add_del, - {'mt_sw_if_index' : tun_sw_if_index, - 'mt_is_add' : is_add, - 'mt_l2_only' : l2_only, - 'mt_next_hop_proto_is_ip4' : next_hop_proto_is_ip4, - 'mt_next_hop_weight' : next_hop_weight, - 'mt_next_hop' : next_hop_address, - 'mt_next_hop_n_out_labels' : next_hop_n_out_labels, - 'mt_next_hop_sw_if_index' :next_hop_sw_if_index, - 'mt_next_hop_table_id' : next_hop_table_id, - 'mt_next_hop_out_label_stack' : next_hop_out_label_stack }) - - return self.api(vpp_papi.sw_interface_span_enable_disable, - (sw_if_index_from, sw_if_index_to, enable)) + {'mt_sw_if_index': tun_sw_if_index, + 'mt_is_add': is_add, + 'mt_l2_only': l2_only, + 'mt_next_hop_proto_is_ip4': next_hop_proto_is_ip4, + 'mt_next_hop_weight': next_hop_weight, + 'mt_next_hop': next_hop_address, + 'mt_next_hop_n_out_labels': next_hop_n_out_labels, + 'mt_next_hop_sw_if_index': next_hop_sw_if_index, + 'mt_next_hop_table_id': next_hop_table_id, + 'mt_next_hop_out_label_stack': next_hop_out_label_stack}) def snat_interface_add_del_feature( self, @@ -723,9 +760,9 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_interface_add_del_feature, - {'is_add' : is_add, - 'is_inside' : is_inside, - 'sw_if_index' : sw_if_index}) + {'is_add': is_add, + 'is_inside': is_inside, + 'sw_if_index': sw_if_index}) def snat_add_static_mapping( self, @@ -750,14 +787,14 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_add_static_mapping, - {'is_add' : is_add, - 'is_ip4' : is_ip4, - 'addr_only' : addr_only, - 'local_ip_address' : local_ip, - 'external_ip_address' : external_ip, - 'local_port' : local_port, - 'external_port' : external_port, - 'vrf_id' : vrf_id}) + {'is_add': is_add, + 'is_ip4': is_ip4, + 'addr_only': addr_only, + 'local_ip_address': local_ip, + 'external_ip_address': external_ip, + 'local_port': local_port, + 'external_port': external_port, + 'vrf_id': vrf_id}) def snat_add_address_range( self, @@ -774,10 +811,10 @@ class VppPapiProvider(object): """ return self.api( self.papi.snat_add_address_range, - {'is_ip4' : is_ip4, - 'first_ip_address' : first_ip_address, - 'last_ip_address' : last_ip_address, - 'is_add' : is_add}) + {'is_ip4': is_ip4, + 'first_ip_address': first_ip_address, + 'last_ip_address': last_ip_address, + 'is_add': is_add}) def snat_address_dump(self): """Dump S-NAT addresses @@ -796,3 +833,43 @@ class VppPapiProvider(object): :return: Dictionary of S-NAT static mappings """ return self.api(self.papi.snat_static_mapping_dump, {}) + + def control_ping(self): + self.api(self.papi.control_ping) + + def bfd_udp_add(self, sw_if_index, desired_min_tx, required_min_rx, + detect_mult, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_add, + { + 'sw_if_index': sw_if_index, + 'desired_min_tx': desired_min_tx, + 'required_min_rx': required_min_rx, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'detect_mult': detect_mult, + }) + + def bfd_udp_del(self, sw_if_index, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_del, + { + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + }) + + def bfd_udp_session_dump(self): + return self.api(self.papi.bfd_udp_session_dump, {}) + + def bfd_session_set_flags(self, bs_idx, admin_up_down): + return self.api(self.papi.bfd_session_set_flags, { + 'bs_index': bs_idx, + 'admin_up_down': admin_up_down, + }) + + def want_bfd_events(self, enable_disable=1): + return self.api(self.papi.want_bfd_events, { + 'enable_disable': enable_disable, + 'pid': os.getpid(), + }) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py index 533c4603..012f5768 100644 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,10 +1,10 @@ import os import time -from scapy.utils import wrpcap, rdpcap +from scapy.utils import wrpcap, rdpcap, PcapReader from vpp_interface import VppInterface from scapy.layers.l2 import Ether, ARP -from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA, \ +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\ ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr from util import ppp @@ -93,6 +93,7 @@ class VppPGInterface(VppInterface): pass # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.capture_cli) + self._pcap_reader = None def add_stream(self, pkts): """ @@ -132,6 +133,41 @@ class VppPGInterface(VppInterface): return [] return output + def wait_for_packet(self, timeout): + """ + Wait for next packet captured with a timeout + + :param timeout: How long to wait for the packet + + :returns: Captured packet if no packet arrived within timeout + :raises Exception: if no packet arrives within timeout + """ + limit = time.time() + timeout + if self._pcap_reader is None: + self.test.logger.debug("Waiting for the capture file to appear") + while time.time() < limit: + if os.path.isfile(self.out_path): + break + time.sleep(0) # yield + if os.path.isfile(self.out_path): + self.test.logger.debug("Capture file appeared after %fs" % + (time.time() - (limit - timeout))) + self._pcap_reader = PcapReader(self.out_path) + else: + self.test.logger.debug("Timeout - capture file still nowhere") + raise Exception("Packet didn't arrive within timeout") + + self.test.logger.debug("Waiting for packet") + while time.time() < limit: + p = self._pcap_reader.recv() + if p is not None: + self.test.logger.debug("Packet received after %fs", + (time.time() - (limit - timeout))) + return p + time.sleep(0) # yield + self.test.logger.debug("Timeout - no packets received") + raise Exception("Packet didn't arrive within timeout") + def create_arp_req(self): """Create ARP request applicable for this interface""" return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / diff --git a/vlib-api/vlibapi/api_helper_macros.h b/vlib-api/vlibapi/api_helper_macros.h index a1475656..16f34cfc 100644 --- a/vlib-api/vlibapi/api_helper_macros.h +++ b/vlib-api/vlibapi/api_helper_macros.h @@ -197,7 +197,8 @@ _(to_netconf_server) \ _(from_netconf_server) \ _(to_netconf_client) \ _(from_netconf_client) \ -_(oam_events) +_(oam_events) \ +_(bfd_events) /* WARNING: replicated in vpp/stats.h */ typedef struct diff --git a/vnet/Makefile.am b/vnet/Makefile.am index eb7f8ef1..4e4b2c0a 100644 --- a/vnet/Makefile.am +++ b/vnet/Makefile.am @@ -349,6 +349,22 @@ nobase_include_HEADERS += \ vnet/ip/udp.h \ vnet/ip/udp_packet.h +######################################## +# Bidirectional Forwarding Detection +######################################## + +nobase_include_HEADERS += \ + vnet/bfd/bfd_protocol.h \ + vnet/bfd/bfd_main.h \ + vnet/bfd/bfd_api.h \ + vnet/bfd/bfd_udp.h + +libvnet_la_SOURCES += \ + vnet/bfd/bfd_api.h \ + vnet/bfd/bfd_udp.c \ + vnet/bfd/bfd_main.c \ + vnet/bfd/bfd_protocol.c + ######################################## # Layer 3 protocol: IPSec ######################################## diff --git a/vnet/vnet/api_errno.h b/vnet/vnet/api_errno.h index 50d6f731..65e3e591 100644 --- a/vnet/vnet/api_errno.h +++ b/vnet/vnet/api_errno.h @@ -90,7 +90,9 @@ _(EXCEEDED_NUMBER_OF_PORTS_CAPACITY, -96, "Operation would exceed capacity of nu _(INVALID_ADDRESS_FAMILY, -97, "Invalid address family") \ _(INVALID_SUB_SW_IF_INDEX, -98, "Invalid sub-interface sw_if_index") \ _(TABLE_TOO_BIG, -99, "Table too big") \ -_(CANNOT_ENABLE_DISABLE_FEATURE, -100, "Cannot enable/disable feature") +_(CANNOT_ENABLE_DISABLE_FEATURE, -100, "Cannot enable/disable feature") \ +_(BFD_EEXIST, -101, "Duplicate BFD session") \ +_(BFD_NOENT, -102, "No such BFD session") typedef enum { diff --git a/vnet/vnet/bfd/bfd_api.h b/vnet/vnet/bfd/bfd_api.h new file mode 100644 index 00000000..cfcd04f3 --- /dev/null +++ b/vnet/vnet/bfd/bfd_api.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_api_h__ +#define __included_bfd_api_h__ + +#include +#include +#include +#include + +vnet_api_error_t bfd_udp_add_session (u32 sw_if_index, u32 desired_min_tx_us, + u32 required_min_rx_us, u8 detect_mult, + const ip46_address_t * local_addr, + const ip46_address_t * peer_addr); + +vnet_api_error_t bfd_udp_del_session (u32 sw_if_index, + const ip46_address_t * local_addr, + const ip46_address_t * peer_addr); + +vnet_api_error_t bfd_session_set_flags (u32 bs_index, u8 admin_up_down); + +#endif /* __included_bfd_api_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_debug.h b/vnet/vnet/bfd/bfd_debug.h new file mode 100644 index 00000000..c11e6d9f --- /dev/null +++ b/vnet/vnet/bfd/bfd_debug.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_debug_h__ +#define __included_bfd_debug_h__ + +/* controls debug prints */ +#define BFD_DEBUG (0) + +#if BFD_DEBUG +#define BFD_DEBUG_FILE_DEF \ + static const char *__file = NULL; \ + if (!__file) \ + { \ + __file = strrchr (__FILE__, '/'); \ + if (__file) \ + { \ + ++__file; \ + } \ + else \ + { \ + __file = __FILE__; \ + } \ + } + +#define BFD_DBG(fmt, ...) \ + do \ + { \ + BFD_DEBUG_FILE_DEF \ + u8 *_s = NULL; \ + vlib_main_t *vm = vlib_get_main (); \ + _s = format (_s, "%6.02f:DBG:%s:%d:%s():" fmt, vlib_time_now (vm), \ + __file, __LINE__, __func__, ##__VA_ARGS__); \ + printf ("%s\n", _s); \ + vec_free (_s); \ + } \ + while (0); + +#define BFD_ERR(fmt, ...) \ + do \ + { \ + BFD_DEBUG_FILE_DEF \ + u8 *_s = NULL; \ + vlib_main_t *vm = vlib_get_main (); \ + _s = format (_s, "%6.02f:ERR:%s:%d:%s():" fmt, vlib_time_now (vm), \ + __file, __LINE__, __func__, ##__VA_ARGS__); \ + printf ("%s\n", _s); \ + vec_free (_s); \ + } \ + while (0); + +#else +#define BFD_DBG(...) +#define BFD_ERR(...) +#endif + +#endif /* __included_bfd_debug_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_doc.md b/vnet/vnet/bfd/bfd_doc.md new file mode 100644 index 00000000..1333ed77 --- /dev/null +++ b/vnet/vnet/bfd/bfd_doc.md @@ -0,0 +1 @@ +TODO diff --git a/vnet/vnet/bfd/bfd_main.c b/vnet/vnet/bfd/bfd_main.c new file mode 100644 index 00000000..a72d6fed --- /dev/null +++ b/vnet/vnet/bfd/bfd_main.c @@ -0,0 +1,928 @@ +/* + * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief BFD nodes implementation + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static u64 +bfd_us_to_clocks (bfd_main_t * bm, u64 us) +{ + return bm->cpu_cps * ((f64) us / USEC_PER_SECOND); +} + +static vlib_node_registration_t bfd_process_node; + +typedef enum +{ +#define F(t, n) BFD_OUTPUT_##t, + foreach_bfd_transport (F) +#undef F + BFD_OUTPUT_N_NEXT, +} bfd_output_next_t; + +static u32 bfd_next_index_by_transport[] = { +#define F(t, n) [BFD_TRANSPORT_##t] = BFD_OUTPUT_##t, + foreach_bfd_transport (F) +#undef F +}; + +/* + * We actually send all bfd pkts to the "error" node after scanning + * them, so the graph node has only one next-index. The "error-drop" + * node automatically bumps our per-node packet counters for us. + */ +typedef enum +{ + BFD_INPUT_NEXT_NORMAL, + BFD_INPUT_N_NEXT, +} bfd_input_next_t; + +static void bfd_on_state_change (bfd_main_t * bm, bfd_session_t * bs, + u64 now); + +static void +bfd_set_defaults (bfd_main_t * bm, bfd_session_t * bs) +{ + bs->local_state = BFD_STATE_down; + bs->local_diag = BFD_DIAG_CODE_no_diag; + bs->remote_state = BFD_STATE_down; + bs->local_demand = 0; + bs->remote_discr = 0; + bs->desired_min_tx_us = BFD_DEFAULT_DESIRED_MIN_TX_US; + bs->desired_min_tx_clocks = bfd_us_to_clocks (bm, bs->desired_min_tx_us); + bs->remote_min_rx_us = 1; + bs->remote_demand = 0; +} + +static void +bfd_set_diag (bfd_session_t * bs, bfd_diag_code_e code) +{ + if (bs->local_diag != code) + { + BFD_DBG ("set local_diag, bs_idx=%d: '%d:%s'", bs->bs_idx, code, + bfd_diag_code_string (code)); + bs->local_diag = code; + } +} + +static void +bfd_set_state (bfd_main_t * bm, bfd_session_t * bs, bfd_state_e new_state) +{ + if (bs->local_state != new_state) + { + BFD_DBG ("Change state, bs_idx=%d: %s->%s", bs->bs_idx, + bfd_state_string (bs->local_state), + bfd_state_string (new_state)); + bs->local_state = new_state; + bfd_on_state_change (bm, bs, clib_cpu_time_now ()); + } +} + +static void +bfd_recalc_tx_interval (bfd_main_t * bm, bfd_session_t * bs) +{ + if (!bs->local_demand) + { + bs->transmit_interval_clocks = + clib_max (bs->desired_min_tx_clocks, bs->remote_min_rx_clocks); + } + else + { + /* TODO */ + } + BFD_DBG ("Recalculated transmit interval %lu clocks/%.2fs", + bs->transmit_interval_clocks, + bs->transmit_interval_clocks / bm->cpu_cps); +} + +static void +bfd_calc_next_tx (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (!bs->local_demand) + { + if (bs->local_detect_mult > 1) + { + /* common case - 75-100% of transmit interval */ + bs->tx_timeout_clocks = now + + (1 - .25 * (random_f64 (&bm->random_seed))) * + bs->transmit_interval_clocks; + } + else + { + /* special case - 75-90% of transmit interval */ + bs->tx_timeout_clocks = + now + + (.9 - .15 * (random_f64 (&bm->random_seed))) * + bs->transmit_interval_clocks; + } + } + else + { + /* TODO */ + } + if (bs->tx_timeout_clocks) + { + BFD_DBG ("Next transmit in %lu clocks/%.02fs@%lu", + bs->tx_timeout_clocks - now, + (bs->tx_timeout_clocks - now) / bm->cpu_cps, + bs->tx_timeout_clocks); + } +} + +static void +bfd_recalc_detection_time (bfd_main_t * bm, bfd_session_t * bs) +{ + if (!bs->local_demand) + { + bs->detection_time_clocks = + bs->remote_detect_mult * + bfd_us_to_clocks (bm, clib_max (bs->required_min_rx_us, + bs->remote_desired_min_tx_us)); + } + else + { + bs->detection_time_clocks = + bs->local_detect_mult * + bfd_us_to_clocks (bm, + clib_max (bs->desired_min_tx_us, + bs->remote_min_rx_us)); + } + BFD_DBG ("Recalculated detection time %lu clocks/%.2fs", + bs->detection_time_clocks, + bs->detection_time_clocks / bm->cpu_cps); +} + +static void +bfd_set_timer (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + u64 next = 0; + u64 rx_timeout = 0; + if (BFD_STATE_up == bs->local_state) + { + rx_timeout = bs->last_rx_clocks + bs->detection_time_clocks; + } + if (bs->tx_timeout_clocks && rx_timeout) + { + next = clib_min (bs->tx_timeout_clocks, rx_timeout); + } + else if (bs->tx_timeout_clocks) + { + next = bs->tx_timeout_clocks; + } + else if (rx_timeout) + { + next = rx_timeout; + } + BFD_DBG ("bs_idx=%u, tx_timeout=%lu, rx_timeout=%lu, next=%s", bs->bs_idx, + bs->tx_timeout_clocks, rx_timeout, + next == bs->tx_timeout_clocks ? "tx" : "rx"); + if (next && (next < bs->wheel_time_clocks || !bs->wheel_time_clocks)) + { + if (bs->wheel_time_clocks) + { + timing_wheel_delete (&bm->wheel, bs->bs_idx); + } + bs->wheel_time_clocks = next; + BFD_DBG ("timing_wheel_insert(%p, %lu (%ld clocks/%.2fs in the " + "future), %u);", + &bm->wheel, bs->wheel_time_clocks, + (i64) bs->wheel_time_clocks - clib_cpu_time_now (), + (i64) (bs->wheel_time_clocks - clib_cpu_time_now ()) / + bm->cpu_cps, bs->bs_idx); + timing_wheel_insert (&bm->wheel, bs->wheel_time_clocks, bs->bs_idx); + } +} + +static void +bfd_set_desired_min_tx (bfd_main_t * bm, bfd_session_t * bs, u64 now, + u32 desired_min_tx_us) +{ + bs->desired_min_tx_us = desired_min_tx_us; + bs->desired_min_tx_clocks = bfd_us_to_clocks (bm, bs->desired_min_tx_us); + BFD_DBG ("Set desired min tx to %uus/%lu clocks/%.2fs", + bs->desired_min_tx_us, bs->desired_min_tx_clocks, + bs->desired_min_tx_clocks / bm->cpu_cps); + bfd_recalc_detection_time (bm, bs); + bfd_recalc_tx_interval (bm, bs); + bfd_calc_next_tx (bm, bs, now); + bfd_set_timer (bm, bs, now); +} + +void +bfd_session_start (bfd_main_t * bm, bfd_session_t * bs) +{ + BFD_DBG ("%U", format_bfd_session, bs); + bfd_recalc_tx_interval (bm, bs); + vlib_process_signal_event (bm->vlib_main, bm->bfd_process_node_index, + BFD_EVENT_NEW_SESSION, bs->bs_idx); +} + +vnet_api_error_t +bfd_del_session (uword bs_idx) +{ + const bfd_main_t *bm = &bfd_main; + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + pool_put (bm->sessions, bs); + return 0; + } + else + { + BFD_ERR ("no such session"); + return VNET_API_ERROR_BFD_NOENT; + } + return 0; +} + +const char * +bfd_diag_code_string (bfd_diag_code_e diag) +{ +#define F(n, t, s) \ + case BFD_DIAG_CODE_NAME (t): \ + return s; + switch (diag) + { + foreach_bfd_diag_code (F)} + return "UNKNOWN"; +#undef F +} + +const char * +bfd_state_string (bfd_state_e state) +{ +#define F(n, t, s) \ + case BFD_STATE_NAME (t): \ + return s; + switch (state) + { + foreach_bfd_state (F)} + return "UNKNOWN"; +#undef F +} + +vnet_api_error_t +bfd_session_set_flags (u32 bs_idx, u8 admin_up_down) +{ + bfd_main_t *bm = &bfd_main; + if (pool_is_free_index (bm->sessions, bs_idx)) + { + BFD_ERR ("invalid bs_idx=%u", bs_idx); + return VNET_API_ERROR_BFD_NOENT; + } + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + if (admin_up_down) + { + bfd_set_state (bm, bs, BFD_STATE_down); + } + else + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_admin_down); + } + return 0; +} + +u8 * +bfd_input_format_trace (u8 * s, va_list * args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + const bfd_input_trace_t *t = va_arg (*args, bfd_input_trace_t *); + const bfd_pkt_t *pkt = (bfd_pkt_t *) t->data; + if (t->len > STRUCT_SIZE_OF (bfd_pkt_t, head)) + { + s = format (s, "BFD v%u, diag=%u(%s), state=%u(%s),\n" + " flags=(P:%u, F:%u, C:%u, A:%u, D:%u, M:%u), detect_mult=%u, " + "length=%u\n", + bfd_pkt_get_version (pkt), bfd_pkt_get_diag_code (pkt), + bfd_diag_code_string (bfd_pkt_get_diag_code (pkt)), + bfd_pkt_get_state (pkt), + bfd_state_string (bfd_pkt_get_state (pkt)), + bfd_pkt_get_poll (pkt), bfd_pkt_get_final (pkt), + bfd_pkt_get_control_plane_independent (pkt), + bfd_pkt_get_auth_present (pkt), bfd_pkt_get_demand (pkt), + bfd_pkt_get_multipoint (pkt), pkt->head.detect_mult, + pkt->head.length); + if (t->len >= sizeof (bfd_pkt_t) + && pkt->head.length >= sizeof (bfd_pkt_t)) + { + s = format (s, " my discriminator: %u\n", pkt->my_disc); + s = format (s, " your discriminator: %u\n", pkt->your_disc); + s = format (s, " desired min tx interval: %u\n", + clib_net_to_host_u32 (pkt->des_min_tx)); + s = format (s, " required min rx interval: %u\n", + clib_net_to_host_u32 (pkt->req_min_rx)); + s = format (s, " required min echo rx interval: %u\n", + clib_net_to_host_u32 (pkt->req_min_echo_rx)); + } + } + + return s; +} + +static void +bfd_on_state_change (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + BFD_DBG ("State changed: %U", format_bfd_session, bs); + bfd_event (bm, bs); + switch (bs->local_state) + { + case BFD_STATE_admin_down: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_down: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_init: + bfd_set_desired_min_tx (bm, bs, now, + clib_max (bs->config_desired_min_tx_us, + BFD_DEFAULT_DESIRED_MIN_TX_US)); + break; + case BFD_STATE_up: + bfd_set_desired_min_tx (bm, bs, now, bs->config_desired_min_tx_us); + break; + } +} + +static void +bfd_add_transport_layer (vlib_main_t * vm, vlib_buffer_t * b, + bfd_session_t * bs) +{ + switch (bs->transport) + { + case BFD_TRANSPORT_UDP4: + /* fallthrough */ + case BFD_TRANSPORT_UDP6: + BFD_DBG ("Transport bfd via udp, bs_idx=%u", bs->bs_idx); + bfd_add_udp_transport (vm, b, &bs->udp); + break; + } +} + +static vlib_buffer_t * +bfd_create_frame (vlib_main_t * vm, vlib_node_runtime_t * rt, + bfd_session_t * bs) +{ + u32 bi; + if (vlib_buffer_alloc (vm, &bi, 1) != 1) + { + clib_warning ("buffer allocation failure"); + return NULL; + } + + vlib_buffer_t *b = vlib_get_buffer (vm, bi); + ASSERT (b->current_data == 0); + + u32 *to_next; + u32 n_left_to_next; + + vlib_get_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], + to_next, n_left_to_next); + + to_next[0] = bi; + n_left_to_next -= 1; + + vlib_put_next_frame (vm, rt, bfd_next_index_by_transport[bs->transport], + n_left_to_next); + return b; +} + +static void +bfd_init_control_frame (vlib_buffer_t * b, bfd_session_t * bs) +{ + bfd_pkt_t *pkt = vlib_buffer_get_current (b); + const u32 bfd_length = 24; + memset (pkt, 0, sizeof (*pkt)); + + bfd_pkt_set_version (pkt, 1); + bfd_pkt_set_diag_code (pkt, bs->local_diag); + bfd_pkt_set_state (pkt, bs->local_state); + if (bs->local_demand && BFD_STATE_up == bs->local_state && + BFD_STATE_up == bs->remote_state) + { + bfd_pkt_set_demand (pkt); + } + pkt->head.detect_mult = bs->local_detect_mult; + pkt->head.length = clib_host_to_net_u32 (bfd_length); + pkt->my_disc = bs->local_discr; + pkt->your_disc = bs->remote_discr; + pkt->des_min_tx = clib_host_to_net_u32 (bs->desired_min_tx_us); + pkt->req_min_rx = clib_host_to_net_u32 (bs->required_min_rx_us); + pkt->req_min_echo_rx = clib_host_to_net_u32 (0); /* FIXME */ + b->current_length = bfd_length; +} + +static void +bfd_send_periodic (vlib_main_t * vm, vlib_node_runtime_t * rt, + bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (!bs->remote_min_rx_us) + { + BFD_DBG + ("bfd.RemoteMinRxInterval is zero, not sending periodic control " + "frame"); + return; + } + /* FIXME + A system MUST NOT periodically transmit BFD Control packets if Demand + mode is active on the remote system (bfd.RemoteDemandMode is 1, + bfd.SessionState is Up, and bfd.RemoteSessionState is Up) and a Poll + Sequence is not being transmitted. + */ + if (now >= bs->tx_timeout_clocks) + { + BFD_DBG ("Send periodic control frame for bs_idx=%lu", bs->bs_idx); + vlib_buffer_t *b = bfd_create_frame (vm, rt, bs); + if (!b) + { + return; + } + bfd_init_control_frame (b, bs); + bfd_add_transport_layer (vm, b, bs); + bfd_calc_next_tx (bm, bs, now); + } + else + { + BFD_DBG ("No need to send control frame now"); + } + bfd_set_timer (bm, bs, now); +} + +void +bfd_send_final (vlib_main_t * vm, vlib_buffer_t * b, bfd_session_t * bs) +{ + BFD_DBG ("Send final control frame for bs_idx=%lu", bs->bs_idx); + bfd_init_control_frame (b, bs); + bfd_pkt_set_final (vlib_buffer_get_current (b)); + bfd_add_transport_layer (vm, b, bs); +} + +static void +bfd_check_rx_timeout (bfd_main_t * bm, bfd_session_t * bs, u64 now) +{ + if (bs->last_rx_clocks + bs->detection_time_clocks < now) + { + BFD_DBG ("Rx timeout, session goes down"); + bfd_set_diag (bs, BFD_DIAG_CODE_det_time_exp); + bfd_set_state (bm, bs, BFD_STATE_down); + } +} + +void +bfd_on_timeout (vlib_main_t * vm, vlib_node_runtime_t * rt, bfd_main_t * bm, + bfd_session_t * bs, u64 now) +{ + BFD_DBG ("Timeout for bs_idx=%lu", bs->bs_idx); + switch (bs->local_state) + { + case BFD_STATE_admin_down: + BFD_ERR ("Unexpected timeout when in %s state", + bfd_state_string (bs->local_state)); + abort (); + break; + case BFD_STATE_down: + bfd_send_periodic (vm, rt, bm, bs, now); + break; + case BFD_STATE_init: + BFD_ERR ("Unexpected timeout when in %s state", + bfd_state_string (bs->local_state)); + abort (); + break; + case BFD_STATE_up: + bfd_check_rx_timeout (bm, bs, now); + bfd_send_periodic (vm, rt, bm, bs, now); + break; + } +} + +/* + * bfd input routine + */ +bfd_error_t +bfd_input (vlib_main_t * vm, vlib_buffer_t * b0, u32 bi0) +{ + // bfd_main_t *bm = &bfd_main; + bfd_error_t e; + + /* find our interface */ + bfd_session_t *s = NULL; + // bfd_get_intf (lm, vnet_buffer (b0)->sw_if_index[VLIB_RX]); + + if (!s) + { + /* bfd disabled on this interface, we're done */ + return BFD_ERROR_DISABLED; + } + + /* Actually scan the packet */ + e = BFD_ERROR_NONE; // bfd_packet_scan (lm, n, vlib_buffer_get_current (b0)); + + return e; +} + +/* + * bfd process node function + */ +static uword +bfd_process (vlib_main_t * vm, vlib_node_runtime_t * rt, vlib_frame_t * f) +{ + bfd_main_t *bm = &bfd_main; + u32 *expired = 0; + uword event_type, *event_data = 0; + + /* So we can send events to the bfd process */ + bm->bfd_process_node_index = bfd_process_node.index; + + while (1) + { + u64 now = clib_cpu_time_now (); + u64 next_expire = timing_wheel_next_expiring_elt_time (&bm->wheel); + BFD_DBG ("timing_wheel_next_expiring_elt_time(%p) returns %lu", + &bm->wheel, next_expire); + if ((i64) next_expire < 0) + { + BFD_DBG ("wait for event without timeout"); + (void) vlib_process_wait_for_event (vm); + } + else + { + f64 timeout = ((i64) next_expire - (i64) now) / bm->cpu_cps; + BFD_DBG ("wait for event with timeout %.02f", timeout); + ASSERT (timeout > 0); + (void) vlib_process_wait_for_event_or_clock (vm, timeout); + } + event_type = vlib_process_get_events (vm, &event_data); + now = clib_cpu_time_now (); + switch (event_type) + { + case ~0: /* no events => timeout */ + /* nothing to do here */ + break; + case BFD_EVENT_RESCHEDULE: + /* nothing to do here - reschedule is done automatically after + * each event or timeout */ + break; + case BFD_EVENT_NEW_SESSION: + do + { + bfd_session_t *bs = + pool_elt_at_index (bm->sessions, *event_data); + bfd_send_periodic (vm, rt, bm, bs, now); + } + while (0); + break; + default: + clib_warning ("BUG: event type 0x%wx", event_type); + break; + } + BFD_DBG ("advancing wheel, now is %lu", now); + BFD_DBG ("timing_wheel_advance (%p, %lu, %p, 0);", &bm->wheel, now, + expired); + expired = timing_wheel_advance (&bm->wheel, now, expired, 0); + BFD_DBG ("Expired %d elements", vec_len (expired)); + u32 *p = NULL; + vec_foreach (p, expired) + { + const u32 bs_idx = *p; + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + bfd_session_t *bs = pool_elt_at_index (bm->sessions, bs_idx); + bs->wheel_time_clocks = 0; /* no longer scheduled */ + bfd_on_timeout (vm, rt, bm, bs, now); + } + } + if (expired) + { + _vec_len (expired) = 0; + } + if (event_data) + { + _vec_len (event_data) = 0; + } + } + + return 0; +} + +/* + * bfd process node declaration + */ +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_process_node, static) = { + .function = bfd_process, + .type = VLIB_NODE_TYPE_PROCESS, + .name = "bfd-process", + .n_next_nodes = BFD_OUTPUT_N_NEXT, + .next_nodes = + { +#define F(t, n) [BFD_OUTPUT_##t] = n, + foreach_bfd_transport (F) +#undef F + }, +}; +/* *INDENT-ON* */ + +static clib_error_t * +bfd_sw_interface_up_down (vnet_main_t * vnm, u32 sw_if_index, u32 flags) +{ + // bfd_main_t *bm = &bfd_main; + // vnet_hw_interface_t *hi = vnet_get_sup_hw_interface (vnm, sw_if_index); + if (!(flags & VNET_SW_INTERFACE_FLAG_ADMIN_UP)) + { + /* TODO */ + } + return 0; +} + +VNET_SW_INTERFACE_ADMIN_UP_DOWN_FUNCTION (bfd_sw_interface_up_down); + +static clib_error_t * +bfd_hw_interface_up_down (vnet_main_t * vnm, u32 hw_if_index, u32 flags) +{ + // bfd_main_t *bm = &bfd_main; + if (flags & VNET_HW_INTERFACE_FLAG_LINK_UP) + { + /* TODO */ + } + return 0; +} + +VNET_HW_INTERFACE_LINK_UP_DOWN_FUNCTION (bfd_hw_interface_up_down); + +/* + * setup function + */ +static clib_error_t * +bfd_main_init (vlib_main_t * vm) +{ + bfd_main_t *bm = &bfd_main; + bm->random_seed = random_default_seed (); + bm->vlib_main = vm; + bm->vnet_main = vnet_get_main (); + memset (&bm->wheel, 0, sizeof (bm->wheel)); + bm->cpu_cps = 2590000000; // vm->clib_time.clocks_per_second; + BFD_DBG ("cps is %.2f", bm->cpu_cps); + const u64 now = clib_cpu_time_now (); + timing_wheel_init (&bm->wheel, now, bm->cpu_cps); + + return 0; +} + +VLIB_INIT_FUNCTION (bfd_main_init); + +bfd_session_t * +bfd_get_session (bfd_main_t * bm, bfd_transport_t t) +{ + bfd_session_t *result; + pool_get (bm->sessions, result); + result->bs_idx = result - bm->sessions; + result->transport = t; + result->local_discr = random_u32 (&bm->random_seed); + bfd_set_defaults (bm, result); + hash_set (bm->session_by_disc, result->local_discr, result->bs_idx); + return result; +} + +void +bfd_put_session (bfd_main_t * bm, bfd_session_t * bs) +{ + hash_unset (bm->session_by_disc, bs->local_discr); + pool_put (bm->sessions, bs); +} + +bfd_session_t * +bfd_find_session_by_idx (bfd_main_t * bm, uword bs_idx) +{ + if (!pool_is_free_index (bm->sessions, bs_idx)) + { + return pool_elt_at_index (bm->sessions, bs_idx); + } + return NULL; +} + +bfd_session_t * +bfd_find_session_by_disc (bfd_main_t * bm, u32 disc) +{ + uword *p = hash_get (bfd_main.session_by_disc, disc); + if (p) + { + return pool_elt_at_index (bfd_main.sessions, *p); + } + return NULL; +} + +/** + * @brief verify bfd packet - common checks + * + * @param pkt + * + * @return 1 if bfd packet is valid + */ +int +bfd_verify_pkt_common (const bfd_pkt_t * pkt) +{ + if (1 != bfd_pkt_get_version (pkt)) + { + BFD_ERR ("BFD verification failed - unexpected version: '%d'", + bfd_pkt_get_version (pkt)); + return 0; + } + if (pkt->head.length < sizeof (bfd_pkt_t) || + (bfd_pkt_get_auth_present (pkt) && + pkt->head.length < sizeof (bfd_pkt_with_auth_t))) + { + BFD_ERR ("BFD verification failed - unexpected length: '%d' (auth " + "present: %d)", + pkt->head.length, bfd_pkt_get_auth_present (pkt)); + return 0; + } + if (!pkt->head.detect_mult) + { + BFD_ERR ("BFD verification failed - unexpected detect-mult: '%d'", + pkt->head.detect_mult); + return 0; + } + if (bfd_pkt_get_multipoint (pkt)) + { + BFD_ERR ("BFD verification failed - unexpected multipoint: '%d'", + bfd_pkt_get_multipoint (pkt)); + return 0; + } + if (!pkt->my_disc) + { + BFD_ERR ("BFD verification failed - unexpected my-disc: '%d'", + pkt->my_disc); + return 0; + } + if (!pkt->your_disc) + { + const u8 pkt_state = bfd_pkt_get_state (pkt); + if (pkt_state != BFD_STATE_down && pkt_state != BFD_STATE_admin_down) + { + BFD_ERR ("BFD verification failed - unexpected state: '%s' " + "(your-disc is zero)", bfd_state_string (pkt_state)); + return 0; + } + } + return 1; +} + +/** + * @brief verify bfd packet - authentication + * + * @param pkt + * + * @return 1 if bfd packet is valid + */ +int +bfd_verify_pkt_session (const bfd_pkt_t * pkt, u16 pkt_size, + const bfd_session_t * bs) +{ + const bfd_pkt_with_auth_t *with_auth = (bfd_pkt_with_auth_t *) pkt; + if (!bfd_pkt_get_auth_present (pkt)) + { + if (pkt_size > sizeof (*pkt)) + { + BFD_ERR ("BFD verification failed - unexpected packet size '%d' " + "(auth not present)", pkt_size); + return 0; + } + } + else + { + if (!with_auth->auth.type) + { + BFD_ERR ("BFD verification failed - unexpected auth type: '%d'", + with_auth->auth.type); + return 0; + } + /* TODO FIXME - implement the actual verification */ + } + return 1; +} + +void +bfd_consume_pkt (bfd_main_t * bm, const bfd_pkt_t * pkt, u32 bs_idx) +{ + bfd_session_t *bs = bfd_find_session_by_idx (bm, bs_idx); + if (!bs) + { + return; + } + BFD_DBG ("Scanning bfd packet, bs_idx=%d", bs->bs_idx); + bs->remote_discr = pkt->my_disc; + bs->remote_state = bfd_pkt_get_state (pkt); + bs->remote_demand = bfd_pkt_get_demand (pkt); + bs->remote_min_rx_us = clib_net_to_host_u32 (pkt->req_min_rx); + bs->remote_min_rx_clocks = bfd_us_to_clocks (bm, bs->remote_min_rx_us); + BFD_DBG ("Set remote min rx to %lu clocks/%.2fs", bs->remote_min_rx_clocks, + bs->remote_min_rx_clocks / bm->cpu_cps); + bs->remote_desired_min_tx_us = clib_net_to_host_u32 (pkt->des_min_tx); + bs->remote_detect_mult = pkt->head.detect_mult; + bfd_recalc_detection_time (bm, bs); + bs->last_rx_clocks = clib_cpu_time_now (); + /* FIXME + If the Required Min Echo RX Interval field is zero, the + transmission of Echo packets, if any, MUST cease. + + If a Poll Sequence is being transmitted by the local system and + the Final (F) bit in the received packet is set, the Poll Sequence + MUST be terminated. + */ + /* FIXME 6.8.2 */ + /* FIXME 6.8.4 */ + if (BFD_STATE_admin_down == bs->local_state) + return; + if (BFD_STATE_admin_down == bs->remote_state) + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_down); + } + else if (BFD_STATE_down == bs->local_state) + { + if (BFD_STATE_down == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_init); + } + else if (BFD_STATE_init == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_up); + } + } + else if (BFD_STATE_init == bs->local_state) + { + if (BFD_STATE_up == bs->remote_state || + BFD_STATE_init == bs->remote_state) + { + bfd_set_state (bm, bs, BFD_STATE_up); + } + } + else /* BFD_STATE_up == bs->local_state */ + { + if (BFD_STATE_down == bs->remote_state) + { + bfd_set_diag (bs, BFD_DIAG_CODE_neighbor_sig_down); + bfd_set_state (bm, bs, BFD_STATE_down); + } + } +} + +u8 * +format_bfd_session (u8 * s, va_list * args) +{ + const bfd_session_t *bs = va_arg (*args, bfd_session_t *); + return format (s, "BFD(%u): bfd.SessionState=%s, " + "bfd.RemoteSessionState=%s, " + "bfd.LocalDiscr=%u, " + "bfd.RemoteDiscr=%u, " + "bfd.LocalDiag=%s, " + "bfd.DesiredMinTxInterval=%u, " + "bfd.RequiredMinRxInterval=%u, " + "bfd.RemoteMinRxInterval=%u, " + "bfd.DemandMode=%s, " + "bfd.RemoteDemandMode=%s, " + "bfd.DetectMult=%u, ", + bs->bs_idx, bfd_state_string (bs->local_state), + bfd_state_string (bs->remote_state), bs->local_discr, + bs->remote_discr, bfd_diag_code_string (bs->local_diag), + bs->desired_min_tx_us, bs->required_min_rx_us, + bs->remote_min_rx_us, (bs->local_demand ? "yes" : "no"), + (bs->remote_demand ? "yes" : "no"), bs->local_detect_mult); +} + +bfd_main_t bfd_main; + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_main.h b/vnet/vnet/bfd/bfd_main.h new file mode 100644 index 00000000..727903bd --- /dev/null +++ b/vnet/vnet/bfd/bfd_main.h @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief BFD global declarations + */ +#ifndef __included_bfd_main_h__ +#define __included_bfd_main_h__ + +#include +#include +#include +#include + +#define foreach_bfd_transport(F) \ + F (UDP4, "ip4-rewrite") \ + F (UDP6, "ip6-rewrite") + +typedef enum +{ +#define F(t, n) BFD_TRANSPORT_##t, + foreach_bfd_transport (F) +#undef F +} bfd_transport_t; + +#define foreach_bfd_mode(F) \ + F (asynchronous) \ + F (demand) + +typedef enum +{ +#define F(x) BFD_MODE_##x, + foreach_bfd_mode (F) +#undef F +} bfd_mode_e; + +typedef struct +{ + /* index in bfd_main.sessions pool */ + uword bs_idx; + + /* session state */ + bfd_state_e local_state; + + /* local diagnostics */ + bfd_diag_code_e local_diag; + + /* remote session state */ + bfd_state_e remote_state; + + /* local discriminator */ + u32 local_discr; + + /* remote discriminator */ + u32 remote_discr; + + /* configured desired min tx interval (microseconds) */ + u32 config_desired_min_tx_us; + + /* desired min tx interval (microseconds) */ + u32 desired_min_tx_us; + + /* desired min tx interval (clocks) */ + u64 desired_min_tx_clocks; + + /* required min rx interval */ + u32 required_min_rx_us; + + /* remote min rx interval (microseconds) */ + u32 remote_min_rx_us; + + /* remote min rx interval (clocks) */ + u64 remote_min_rx_clocks; + + /* remote desired min tx interval */ + u32 remote_desired_min_tx_us; + + /* 1 if in demand mode, 0 otherwise */ + u8 local_demand; + + /* 1 if remote system sets demand mode, 0 otherwise */ + u8 remote_demand; + + u8 local_detect_mult; + u8 remote_detect_mult; + + /* set to value of timer in timing wheel, 0 if not set */ + u64 wheel_time_clocks; + + /* transmit interval */ + u64 transmit_interval_clocks; + + /* next time at which to transmit a packet */ + u64 tx_timeout_clocks; + + /* timestamp of last packet received */ + u64 last_rx_clocks; + + /* detection time */ + u64 detection_time_clocks; + + /* transport type for this session */ + bfd_transport_t transport; + + union + { + bfd_udp_session_t udp; + }; +} bfd_session_t; + +typedef struct +{ + u32 client_index; + u32 client_pid; +} event_subscriber_t; + +typedef struct +{ + /* pool of bfd sessions context data */ + bfd_session_t *sessions; + + /* timing wheel for scheduling timeouts */ + timing_wheel_t wheel; + + /* hashmap - bfd session by discriminator */ + u32 *session_by_disc; + + /* background process node index */ + u32 bfd_process_node_index; + + /* convenience variables */ + vlib_main_t *vlib_main; + vnet_main_t *vnet_main; + + /* cpu clocks per second */ + f64 cpu_cps; + + /* for generating random numbers */ + u32 random_seed; + + /* pool of event subscribers */ + //event_subscriber_t *subscribers; + +} bfd_main_t; + +extern bfd_main_t bfd_main; + +/* Packet counters */ +#define foreach_bfd_error(F) \ + F (NONE, "good bfd packets (processed)") \ + F (BAD, "invalid bfd packets") \ + F (DISABLED, "bfd packets received on disabled interfaces") + +typedef enum +{ +#define F(sym, str) BFD_ERROR_##sym, + foreach_bfd_error (F) +#undef F + BFD_N_ERROR, +} bfd_error_t; + +/* bfd packet trace capture */ +typedef struct +{ + u32 len; + u8 data[400]; +} bfd_input_trace_t; + +enum +{ + BFD_EVENT_RESCHEDULE = 1, + BFD_EVENT_NEW_SESSION, +} bfd_process_event_e; + +bfd_error_t bfd_input (vlib_main_t * vm, vlib_buffer_t * b0, u32 bi0); +u8 *bfd_input_format_trace (u8 * s, va_list * args); + +bfd_session_t *bfd_get_session (bfd_main_t * bm, bfd_transport_t t); +void bfd_put_session (bfd_main_t * bm, bfd_session_t * bs); +bfd_session_t *bfd_find_session_by_idx (bfd_main_t * bm, uword bs_idx); +bfd_session_t *bfd_find_session_by_disc (bfd_main_t * bm, u32 disc); +void bfd_session_start (bfd_main_t * bm, bfd_session_t * bs); +void bfd_consume_pkt (bfd_main_t * bm, const bfd_pkt_t * bfd, u32 bs_idx); +int bfd_verify_pkt_common (const bfd_pkt_t * pkt); +int bfd_verify_pkt_session (const bfd_pkt_t * pkt, u16 pkt_size, + const bfd_session_t * bs); +void bfd_event (bfd_main_t * bm, bfd_session_t * bs); +void bfd_send_final (vlib_main_t * vm, vlib_buffer_t * b, bfd_session_t * bs); +u8 *format_bfd_session (u8 * s, va_list * args); + + +#define USEC_PER_MS 1000LL +#define USEC_PER_SECOND (1000 * USEC_PER_MS) + +/* default, slow transmission interval for BFD packets, per spec at least 1s */ +#define BFD_DEFAULT_DESIRED_MIN_TX_US USEC_PER_SECOND + +#endif /* __included_bfd_main_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_protocol.c b/vnet/vnet/bfd/bfd_protocol.c new file mode 100644 index 00000000..ede9536f --- /dev/null +++ b/vnet/vnet/bfd/bfd_protocol.c @@ -0,0 +1,74 @@ +#include + +u8 bfd_pkt_get_version (const bfd_pkt_t *pkt) +{ + return pkt->head.vers_diag >> 5; +} + +void bfd_pkt_set_version (bfd_pkt_t *pkt, int version) +{ + pkt->head.vers_diag = + (version << 5) | (pkt->head.vers_diag & ((1 << 5) - 1)); +} + +u8 bfd_pkt_get_diag_code (const bfd_pkt_t *pkt) +{ + return pkt->head.vers_diag & ((1 << 5) - 1); +} + +void bfd_pkt_set_diag_code (bfd_pkt_t *pkt, int value) +{ + pkt->head.vers_diag = + (pkt->head.vers_diag & ~((1 << 5) - 1)) | (value & ((1 << 5) - 1)); +} + +u8 bfd_pkt_get_state (const bfd_pkt_t *pkt) +{ + return pkt->head.sta_flags >> 6; +} + +void bfd_pkt_set_state (bfd_pkt_t *pkt, int value) +{ + pkt->head.sta_flags = (value << 6) | (pkt->head.sta_flags & ((1 << 6) - 1)); +} + +u8 bfd_pkt_get_poll (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 5) & 1; +} + +void bfd_pkt_set_final (bfd_pkt_t *pkt) { pkt->head.sta_flags |= 1 << 5; } + +u8 bfd_pkt_get_final (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 4) & 1; +} + +void bfd_pkt_set_poll (bfd_pkt_t *pkt); +u8 bfd_pkt_get_control_plane_independent (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 3) & 1; +} + +void bfd_pkt_set_control_plane_independent (bfd_pkt_t *pkt); + +u8 bfd_pkt_get_auth_present (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 2) & 1; +} + +void bfd_pkt_set_auth_present (bfd_pkt_t *pkt); + +u8 bfd_pkt_get_demand (const bfd_pkt_t *pkt) +{ + return (pkt->head.sta_flags >> 1) & 1; +} + +void bfd_pkt_set_demand (bfd_pkt_t *pkt) { pkt->head.sta_flags |= 1 << 1; } + +u8 bfd_pkt_get_multipoint (const bfd_pkt_t *pkt) +{ + return pkt->head.sta_flags & 1; +} + +void bfd_pkt_set_multipoint (bfd_pkt_t *pkt); diff --git a/vnet/vnet/bfd/bfd_protocol.h b/vnet/vnet/bfd/bfd_protocol.h new file mode 100644 index 00000000..cf751b3b --- /dev/null +++ b/vnet/vnet/bfd/bfd_protocol.h @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef __included_bfd_protocol_h__ +#define __included_bfd_protocol_h__ +/** + * @file + * @brief BFD protocol declarations + */ + +#include +#include + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + /* + An optional Authentication Section MAY be present: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Auth Type | Auth Len | Authentication Data... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + u8 type; + u8 len; + u8 data[0]; +}) bfd_auth_t; +/* *INDENT-ON* */ + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + /* + The Mandatory Section of a BFD Control packet has the following + format: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Vers | Diag |Sta|P|F|C|A|D|M| Detect Mult | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | My Discriminator | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Your Discriminator | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Desired Min TX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Required Min RX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Required Min Echo RX Interval | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + struct + { + u8 vers_diag; + u8 sta_flags; + u8 detect_mult; + u8 length; + } head; + u32 my_disc; + u32 your_disc; + u32 des_min_tx; + u32 req_min_rx; + u32 req_min_echo_rx; +}) bfd_pkt_t; +/* *INDENT-ON* */ + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + bfd_pkt_t pkt; + bfd_auth_t auth; +}) bfd_pkt_with_auth_t; +/* *INDENT-ON* */ + +u8 bfd_pkt_get_version (const bfd_pkt_t * pkt); +void bfd_pkt_set_version (bfd_pkt_t * pkt, int version); +u8 bfd_pkt_get_diag_code (const bfd_pkt_t * pkt); +void bfd_pkt_set_diag_code (bfd_pkt_t * pkt, int value); +u8 bfd_pkt_get_state (const bfd_pkt_t * pkt); +void bfd_pkt_set_state (bfd_pkt_t * pkt, int value); +u8 bfd_pkt_get_poll (const bfd_pkt_t * pkt); +void bfd_pkt_set_final (bfd_pkt_t * pkt); +u8 bfd_pkt_get_final (const bfd_pkt_t * pkt); +void bfd_pkt_set_poll (bfd_pkt_t * pkt); +u8 bfd_pkt_get_control_plane_independent (const bfd_pkt_t * pkt); +void bfd_pkt_set_control_plane_independent (bfd_pkt_t * pkt); +u8 bfd_pkt_get_auth_present (const bfd_pkt_t * pkt); +void bfd_pkt_set_auth_present (bfd_pkt_t * pkt); +u8 bfd_pkt_get_demand (const bfd_pkt_t * pkt); +void bfd_pkt_set_demand (bfd_pkt_t * pkt); +u8 bfd_pkt_get_multipoint (const bfd_pkt_t * pkt); +void bfd_pkt_set_multipoint (bfd_pkt_t * pkt); + +/* BFD diagnostic codes */ +#define foreach_bfd_diag_code(F) \ + F (0, no_diag, "No Diagnostic") \ + F (1, det_time_exp, "Control Detection Time Expired") \ + F (2, echo_failed, "Echo Function Failed") \ + F (3, neighbor_sig_down, "Neighbor Signaled Session Down") \ + F (4, fwd_plain_reset, "Forwarding Plane Reset") \ + F (5, path_down, "Path Down") \ + F (6, concat_path_down, "Concatenated Path Down") \ + F (7, admin_down, "Administratively Down") \ + F (8, reverse_concat_path_down, "Reverse Concatenated Path Down") + +#define BFD_DIAG_CODE_NAME(t) BFD_DIAG_CODE_##t + +typedef enum +{ +#define F(n, t, s) BFD_DIAG_CODE_NAME (t) = n, + foreach_bfd_diag_code (F) +#undef F +} bfd_diag_code_e; + +const char *bfd_diag_code_string (bfd_diag_code_e diag); + +/* BFD state values */ +#define foreach_bfd_state(F) \ + F (0, admin_down, "AdminDown") \ + F (1, down, "Down") \ + F (2, init, "Init") \ + F (3, up, "Up") + +#define BFD_STATE_NAME(t) BFD_STATE_##t + +typedef enum +{ +#define F(n, t, s) BFD_STATE_NAME (t) = n, + foreach_bfd_state (F) +#undef F +} bfd_state_e; + +const char *bfd_state_string (bfd_state_e state); + +#endif /* __included_bfd_protocol_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/bfd_udp.c b/vnet/vnet/bfd/bfd_udp.c new file mode 100644 index 00000000..ded33425 --- /dev/null +++ b/vnet/vnet/bfd/bfd_udp.c @@ -0,0 +1,610 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + bfd_main_t *bfd_main; + /* hashmap - bfd session index by bfd key - used for CLI/API lookup, where + * discriminator is unknown */ + mhash_t bfd_session_idx_by_bfd_key; +} bfd_udp_main_t; + +bfd_udp_main_t bfd_udp_main; + +void bfd_udp_transport_to_buffer (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) +{ + udp_header_t *udp; + u16 udp_length, ip_length; + bfd_udp_key_t *key = &bus->key; + + b->flags |= VNET_BUFFER_LOCALLY_ORIGINATED; + if (ip46_address_is_ip4 (&key->local_addr)) + { + ip4_header_t *ip4; + const size_t data_size = sizeof (*ip4) + sizeof (*udp); + vlib_buffer_advance (b, -data_size); + ip4 = vlib_buffer_get_current (b); + udp = (udp_header_t *)(ip4 + 1); + memset (ip4, 0, data_size); + ip4->ip_version_and_header_length = 0x45; + ip4->ttl = 255; + ip4->protocol = IP_PROTOCOL_UDP; + ip4->src_address.as_u32 = key->local_addr.ip4.as_u32; + ip4->dst_address.as_u32 = key->peer_addr.ip4.as_u32; + + udp->src_port = clib_host_to_net_u16 (50000); /* FIXME */ + udp->dst_port = clib_host_to_net_u16 (UDP_DST_PORT_bfd4); + + /* fix ip length, checksum and udp length */ + ip_length = vlib_buffer_length_in_chain (vm, b); + + ip4->length = clib_host_to_net_u16 (ip_length); + ip4->checksum = ip4_header_checksum (ip4); + + udp_length = ip_length - (sizeof (*ip4)); + udp->length = clib_host_to_net_u16 (udp_length); + } + else + { + BFD_ERR ("not implemented"); + abort (); + } +} + +void bfd_add_udp_transport (vlib_main_t *vm, vlib_buffer_t *b, + bfd_udp_session_t *bus) +{ + vnet_buffer (b)->ip.adj_index[VLIB_RX] = bus->adj_index; + vnet_buffer (b)->ip.adj_index[VLIB_TX] = bus->adj_index; + bfd_udp_transport_to_buffer (vm, b, bus); +} + +static bfd_session_t *bfd_lookup_session (bfd_udp_main_t *bum, + const bfd_udp_key_t *key) +{ + uword *p = mhash_get (&bum->bfd_session_idx_by_bfd_key, key); + if (p) + { + return bfd_find_session_by_idx (bum->bfd_main, *p); + } + return 0; +} + +static vnet_api_error_t +bfd_udp_add_session_internal (bfd_udp_main_t *bum, u32 sw_if_index, + u32 desired_min_tx_us, u32 required_min_rx_us, + u8 detect_mult, const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + /* get a pool entry and if we end up not needing it, give it back */ + bfd_transport_t t = BFD_TRANSPORT_UDP4; + if (!ip46_address_is_ip4 (local_addr)) + { + t = BFD_TRANSPORT_UDP6; + } + bfd_session_t *bs = bfd_get_session (bum->bfd_main, t); + bfd_udp_session_t *bus = &bs->udp; + memset (bus, 0, sizeof (*bus)); + bfd_udp_key_t *key = &bus->key; + key->sw_if_index = sw_if->sw_if_index; + key->local_addr.as_u64[0] = local_addr->as_u64[0]; + key->local_addr.as_u64[1] = local_addr->as_u64[1]; + key->peer_addr.as_u64[0] = peer_addr->as_u64[0]; + key->peer_addr.as_u64[1] = peer_addr->as_u64[1]; + const bfd_session_t *tmp = bfd_lookup_session (bum, key); + if (tmp) + { + BFD_ERR ("duplicate bfd-udp session, existing bs_idx=%d", tmp->bs_idx); + bfd_put_session (bum->bfd_main, bs); + return VNET_API_ERROR_BFD_EEXIST; + } + key->sw_if_index = sw_if->sw_if_index; + mhash_set (&bum->bfd_session_idx_by_bfd_key, key, bs->bs_idx, NULL); + BFD_DBG ("session created, bs_idx=%u, sw_if_index=%d, local=%U, peer=%U", + bs->bs_idx, key->sw_if_index, format_ip46_address, &key->local_addr, + IP46_TYPE_ANY, format_ip46_address, &key->peer_addr, IP46_TYPE_ANY); + if (BFD_TRANSPORT_UDP4 == t) + { + bus->adj_index = adj_nbr_add_or_lock (FIB_PROTOCOL_IP4, VNET_LINK_IP4, + &key->peer_addr, key->sw_if_index); + BFD_DBG ("adj_nbr_add_or_lock(FIB_PROTOCOL_IP4, VNET_LINK_IP4, %U, %d) " + "returns %d", + format_ip46_address, &key->peer_addr, IP46_TYPE_ANY, + key->sw_if_index, bus->adj_index); + } + else + { + bus->adj_index = adj_nbr_add_or_lock (FIB_PROTOCOL_IP6, VNET_LINK_IP6, + &key->peer_addr, key->sw_if_index); + BFD_DBG ("adj_nbr_add_or_lock(FIB_PROTOCOL_IP6, VNET_LINK_IP6, %U, %d) " + "returns %d", + format_ip46_address, &key->peer_addr, IP46_TYPE_ANY, + key->sw_if_index, bus->adj_index); + } + bs->config_desired_min_tx_us = desired_min_tx_us; + bs->required_min_rx_us = required_min_rx_us; + bs->local_detect_mult = detect_mult; + bfd_session_start (bum->bfd_main, bs); + return 0; +} + +static vnet_api_error_t +bfd_udp_validate_api_input (u32 sw_if_index, const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + u8 local_ip_valid = 0; + ip_interface_address_t *ia = NULL; + if (!sw_if) + { + BFD_ERR ("got NULL sw_if"); + return VNET_API_ERROR_INVALID_SW_IF_INDEX; + } + if (ip46_address_is_ip4 (local_addr)) + { + if (!ip46_address_is_ip4 (peer_addr)) + { + BFD_ERR ("IP family mismatch"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + ip4_main_t *im = &ip4_main; + + /* *INDENT-OFF* */ + foreach_ip_interface_address ( + &im->lookup_main, ia, sw_if_index, 0 /* honor unnumbered */, ({ + ip4_address_t *x = + ip_interface_address_get_address (&im->lookup_main, ia); + if (x->as_u32 == local_addr->ip4.as_u32) + { + /* valid address for this interface */ + local_ip_valid = 1; + break; + } + })); + /* *INDENT-ON* */ + } + else + { + if (ip46_address_is_ip4 (peer_addr)) + { + BFD_ERR ("IP family mismatch"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + ip6_main_t *im = &ip6_main; + /* *INDENT-OFF* */ + foreach_ip_interface_address ( + &im->lookup_main, ia, sw_if_index, 0 /* honor unnumbered */, ({ + ip6_address_t *x = + ip_interface_address_get_address (&im->lookup_main, ia); + if (local_addr->ip6.as_u64[0] == x->as_u64[0] && + local_addr->ip6.as_u64[1] == x->as_u64[1]) + { + /* valid address for this interface */ + local_ip_valid = 1; + break; + } + })); + /* *INDENT-ON* */ + } + + if (!local_ip_valid) + { + BFD_ERR ("address not found on interface"); + return VNET_API_ERROR_ADDRESS_NOT_FOUND_FOR_INTERFACE; + } + + return 0; +} + +vnet_api_error_t bfd_udp_add_session (u32 sw_if_index, u32 desired_min_tx_us, + u32 required_min_rx_us, u8 detect_mult, + const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_api_error_t rv = + bfd_udp_validate_api_input (sw_if_index, local_addr, peer_addr); + if (rv) + { + return rv; + } + if (detect_mult < 1) + { + BFD_ERR ("detect_mult < 1"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + if (desired_min_tx_us < 1) + { + BFD_ERR ("desired_min_tx_us < 1"); + return VNET_API_ERROR_INVALID_ARGUMENT; + } + return bfd_udp_add_session_internal (&bfd_udp_main, sw_if_index, + desired_min_tx_us, required_min_rx_us, + detect_mult, local_addr, peer_addr); +} + +vnet_api_error_t bfd_udp_del_session (u32 sw_if_index, + const ip46_address_t *local_addr, + const ip46_address_t *peer_addr) +{ + vnet_api_error_t rv = + bfd_udp_validate_api_input (sw_if_index, local_addr, peer_addr); + if (rv) + { + return rv; + } + bfd_udp_main_t *bum = &bfd_udp_main; + vnet_sw_interface_t *sw_if = + vnet_get_sw_interface (vnet_get_main (), sw_if_index); + bfd_udp_key_t key; + memset (&key, 0, sizeof (key)); + key.sw_if_index = sw_if->sw_if_index; + key.local_addr.as_u64[0] = local_addr->as_u64[0]; + key.local_addr.as_u64[1] = local_addr->as_u64[1]; + key.peer_addr.as_u64[0] = peer_addr->as_u64[0]; + key.peer_addr.as_u64[1] = peer_addr->as_u64[1]; + bfd_session_t *tmp = bfd_lookup_session (bum, &key); + if (tmp) + { + BFD_DBG ("free bfd-udp session, bs_idx=%d", tmp->bs_idx); + mhash_unset (&bum->bfd_session_idx_by_bfd_key, &key, NULL); + adj_unlock (tmp->udp.adj_index); + bfd_put_session (bum->bfd_main, tmp); + } + else + { + BFD_ERR ("no such session"); + return VNET_API_ERROR_BFD_NOENT; + } + return 0; +} + +typedef enum { + BFD_UDP_INPUT_NEXT_NORMAL, + BFD_UDP_INPUT_NEXT_REPLY, + BFD_UDP_INPUT_N_NEXT, +} bfd_udp_input_next_t; + +/* Packet counters */ +#define foreach_bfd_udp_error(F) \ + F (NONE, "good bfd packets (processed)") \ + F (BAD, "invalid bfd packets") \ + F (DISABLED, "bfd packets received on disabled interfaces") + +#define F(sym, string) static char BFD_UDP_ERR_##sym##_STR[] = string; +foreach_bfd_udp_error (F); +#undef F + +static char *bfd_udp_error_strings[] = { +#define F(sym, string) BFD_UDP_ERR_##sym##_STR, + foreach_bfd_udp_error (F) +#undef F +}; + +typedef enum { +#define F(sym, str) BFD_UDP_ERROR_##sym, + foreach_bfd_udp_error (F) +#undef F + BFD_UDP_N_ERROR, +} bfd_udp_error_t; + +static void bfd_udp4_find_headers (vlib_buffer_t *b, const ip4_header_t **ip4, + const udp_header_t **udp) +{ + *ip4 = vnet_buffer (b)->ip.header; + *udp = (udp_header_t *)((*ip4) + 1); +} + +static bfd_udp_error_t bfd_udp4_verify_transport (const ip4_header_t *ip4, + const udp_header_t *udp, + const bfd_session_t *bs) +{ + const bfd_udp_session_t *bus = &bs->udp; + const bfd_udp_key_t *key = &bus->key; + if (ip4->src_address.as_u32 != key->peer_addr.ip4.as_u32) + { + BFD_ERR ("IP src addr mismatch, got %U, expected %U", format_ip4_address, + ip4->src_address.as_u32, format_ip4_address, + key->peer_addr.ip4.as_u32); + return BFD_UDP_ERROR_BAD; + } + if (ip4->dst_address.as_u32 != key->local_addr.ip4.as_u32) + { + BFD_ERR ("IP dst addr mismatch, got %U, expected %U", format_ip4_address, + ip4->dst_address.as_u32, format_ip4_address, + key->local_addr.ip4.as_u32); + return BFD_UDP_ERROR_BAD; + } + const u8 expected_ttl = 255; + if (ip4->ttl != expected_ttl) + { + BFD_ERR ("IP unexpected TTL value %d, expected %d", ip4->ttl, + expected_ttl); + return BFD_UDP_ERROR_BAD; + } + if (clib_net_to_host_u16 (udp->src_port) < 49152 || + clib_net_to_host_u16 (udp->src_port) > 65535) + { + BFD_ERR ("Invalid UDP src port %d, out of range <49152,65535>", + udp->src_port); + } + return BFD_UDP_ERROR_NONE; +} + +typedef struct +{ + u32 bs_idx; + bfd_pkt_t pkt; +} bfd_rpc_update_t; + +static void bfd_rpc_update_session_cb (const bfd_rpc_update_t *a) +{ + bfd_consume_pkt (bfd_udp_main.bfd_main, &a->pkt, a->bs_idx); +} + +static void bfd_rpc_update_session (u32 bs_idx, const bfd_pkt_t *pkt) +{ + /* packet length was already verified to be correct by the caller */ + const u32 data_size = sizeof (bfd_rpc_update_t) - + STRUCT_SIZE_OF (bfd_rpc_update_t, pkt) + + pkt->head.length; + u8 data[data_size]; + bfd_rpc_update_t *update = (bfd_rpc_update_t *)data; + update->bs_idx = bs_idx; + clib_memcpy (&update->pkt, pkt, pkt->head.length); + vl_api_rpc_call_main_thread (bfd_rpc_update_session_cb, data, data_size); +} + +static bfd_udp_error_t bfd_udp4_scan (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_buffer_t *b, bfd_session_t **bs_out) +{ + const bfd_pkt_t *pkt = vlib_buffer_get_current (b); + if (sizeof (*pkt) > b->current_length) + { + BFD_ERR ( + "Payload size %d too small to hold bfd packet of minimum size %d", + b->current_length, sizeof (*pkt)); + return BFD_UDP_ERROR_BAD; + } + const ip4_header_t *ip4; + const udp_header_t *udp; + bfd_udp4_find_headers (b, &ip4, &udp); + if (!ip4 || !udp) + { + BFD_ERR ("Couldn't find ip4 or udp header"); + return BFD_UDP_ERROR_BAD; + } + if (!bfd_verify_pkt_common (pkt)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_session_t *bs = NULL; + if (pkt->your_disc) + { + BFD_DBG ("Looking up BFD session using discriminator %u", + pkt->your_disc); + bs = bfd_find_session_by_disc (bfd_udp_main.bfd_main, pkt->your_disc); + } + else + { + bfd_udp_key_t key; + memset (&key, 0, sizeof (key)); + key.sw_if_index = vnet_buffer (b)->sw_if_index[VLIB_RX]; + key.local_addr.ip4.as_u32 = ip4->dst_address.as_u32; + key.peer_addr.ip4.as_u32 = ip4->src_address.as_u32; + BFD_DBG ("Looking up BFD session using key (sw_if_index=%u, local=%U, " + "peer=%U)", + key.sw_if_index, format_ip4_address, key.local_addr.ip4.as_u8, + format_ip4_address, key.peer_addr.ip4.as_u8); + bs = bfd_lookup_session (&bfd_udp_main, &key); + } + if (!bs) + { + BFD_ERR ("BFD session lookup failed - no session matches BFD pkt"); + return BFD_UDP_ERROR_BAD; + } + BFD_DBG ("BFD session found, bs_idx=%d", bs->bs_idx); + if (!bfd_verify_pkt_session (pkt, b->current_length, bs)) + { + return BFD_UDP_ERROR_BAD; + } + bfd_udp_error_t err; + if (BFD_UDP_ERROR_NONE != (err = bfd_udp4_verify_transport (ip4, udp, bs))) + { + return err; + } + bfd_rpc_update_session (bs->bs_idx, pkt); + *bs_out = bs; + return BFD_UDP_ERROR_NONE; +} + +static bfd_udp_error_t bfd_udp6_scan (vlib_main_t *vm, vlib_buffer_t *b) +{ + /* TODO */ + return BFD_UDP_ERROR_BAD; +} + +/* + * Process a frame of bfd packets + * Expect 1 packet / frame + */ +static uword bfd_udp_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f, int is_ipv6) +{ + u32 n_left_from, *from; + bfd_input_trace_t *t0; + + from = vlib_frame_vector_args (f); /* array of buffer indices */ + n_left_from = f->n_vectors; /* number of buffer indices */ + + while (n_left_from > 0) + { + u32 bi0; + vlib_buffer_t *b0; + u32 next0, error0; + + bi0 = from[0]; + b0 = vlib_get_buffer (vm, bi0); + + bfd_session_t *bs = NULL; + + /* If this pkt is traced, snapshot the data */ + if (b0->flags & VLIB_BUFFER_IS_TRACED) + { + int len; + t0 = vlib_add_trace (vm, rt, b0, sizeof (*t0)); + len = (b0->current_length < sizeof (t0->data)) ? b0->current_length + : sizeof (t0->data); + t0->len = len; + clib_memcpy (t0->data, vlib_buffer_get_current (b0), len); + } + + /* scan this bfd pkt. error0 is the counter index to bmp */ + if (is_ipv6) + { + error0 = bfd_udp6_scan (vm, b0); + } + else + { + error0 = bfd_udp4_scan (vm, rt, b0, &bs); + } + b0->error = rt->errors[error0]; + + next0 = BFD_UDP_INPUT_NEXT_NORMAL; + if (BFD_UDP_ERROR_NONE == error0) + { + /* if everything went fine, check for poll bit, if present, re-use + the buffer and based on (now update) session parameters, send the + final packet back */ + const bfd_pkt_t *pkt = vlib_buffer_get_current (b0); + if (bfd_pkt_get_poll (pkt)) + { + bfd_send_final (vm, b0, bs); + next0 = BFD_UDP_INPUT_NEXT_REPLY; + } + } + vlib_set_next_frame_buffer (vm, rt, next0, bi0); + + from += 1; + n_left_from -= 1; + } + + return f->n_vectors; +} + +static uword bfd_udp4_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f) +{ + return bfd_udp_input (vm, rt, f, 0); +} + +/* + * bfd input graph node declaration + */ +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_udp4_input_node, static) = { + .function = bfd_udp4_input, + .name = "bfd-udp4-input", + .vector_size = sizeof (u32), + .type = VLIB_NODE_TYPE_INTERNAL, + + .n_errors = BFD_UDP_N_ERROR, + .error_strings = bfd_udp_error_strings, + + .format_trace = bfd_input_format_trace, + + .n_next_nodes = BFD_UDP_INPUT_N_NEXT, + .next_nodes = + { + [BFD_UDP_INPUT_NEXT_NORMAL] = "error-drop", + [BFD_UDP_INPUT_NEXT_REPLY] = "ip4-lookup", + }, +}; +/* *INDENT-ON* */ + +static uword bfd_udp6_input (vlib_main_t *vm, vlib_node_runtime_t *rt, + vlib_frame_t *f) +{ + return bfd_udp_input (vm, rt, f, 1); +} + +/* *INDENT-OFF* */ +VLIB_REGISTER_NODE (bfd_udp6_input_node, static) = { + .function = bfd_udp6_input, + .name = "bfd-udp6-input", + .vector_size = sizeof (u32), + .type = VLIB_NODE_TYPE_INTERNAL, + + .n_errors = BFD_UDP_N_ERROR, + .error_strings = bfd_udp_error_strings, + + .format_trace = bfd_input_format_trace, + + .n_next_nodes = BFD_UDP_INPUT_N_NEXT, + .next_nodes = + { + [BFD_UDP_INPUT_NEXT_NORMAL] = "error-drop", + [BFD_UDP_INPUT_NEXT_REPLY] = "ip6-lookup", + }, +}; +/* *INDENT-ON* */ + +static clib_error_t *bfd_sw_interface_up_down (vnet_main_t *vnm, + u32 sw_if_index, u32 flags) +{ + // vnet_hw_interface_t *hi = vnet_get_sup_hw_interface (vnm, sw_if_index); + if (!(flags & VNET_SW_INTERFACE_FLAG_ADMIN_UP)) + { + /* TODO */ + } + return 0; +} + +VNET_SW_INTERFACE_ADMIN_UP_DOWN_FUNCTION (bfd_sw_interface_up_down); + +static clib_error_t *bfd_hw_interface_up_down (vnet_main_t *vnm, + u32 hw_if_index, u32 flags) +{ + if (flags & VNET_HW_INTERFACE_FLAG_LINK_UP) + { + /* TODO */ + } + return 0; +} + +VNET_HW_INTERFACE_LINK_UP_DOWN_FUNCTION (bfd_hw_interface_up_down); + +/* + * setup function + */ +static clib_error_t *bfd_udp_init (vlib_main_t *vm) +{ + mhash_init (&bfd_udp_main.bfd_session_idx_by_bfd_key, sizeof (uword), + sizeof (bfd_udp_key_t)); + bfd_udp_main.bfd_main = &bfd_main; + udp_register_dst_port (vm, UDP_DST_PORT_bfd4, bfd_udp4_input_node.index, 1); + udp_register_dst_port (vm, UDP_DST_PORT_bfd6, bfd_udp6_input_node.index, 0); + return 0; +} + +VLIB_INIT_FUNCTION (bfd_udp_init); diff --git a/vnet/vnet/bfd/bfd_udp.h b/vnet/vnet/bfd/bfd_udp.h new file mode 100644 index 00000000..51f5327b --- /dev/null +++ b/vnet/vnet/bfd/bfd_udp.h @@ -0,0 +1,56 @@ +/* * Copyright (c) 2011-2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief BFD global declarations + */ + +#ifndef __included_bfd_udp_h__ +#define __included_bfd_udp_h__ + +#include +#include +#include + +#define BFD_UDP_KEY_BODY + +/* *INDENT-OFF* */ +typedef CLIB_PACKED (struct { + + u32 sw_if_index; + ip46_address_t local_addr; + ip46_address_t peer_addr; + +}) bfd_udp_key_t; +/* *INDENT-ON* */ + +typedef struct +{ + bfd_udp_key_t key; + + adj_index_t adj_index; +} bfd_udp_session_t; + +void bfd_add_udp_transport (vlib_main_t * vm, vlib_buffer_t * b, + bfd_udp_session_t * bs); + +#endif /* __included_bfd_udp_h__ */ + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/vnet/vnet/bfd/dir.dox b/vnet/vnet/bfd/dir.dox new file mode 100644 index 00000000..ed656b52 --- /dev/null +++ b/vnet/vnet/bfd/dir.dox @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + @dir vnet/vnet/bfd + @brief Bidirectional Forwarding Detection (BFD) implementation +*/ diff --git a/vnet/vnet/buffer.h b/vnet/vnet/buffer.h index 898c94ee..82806bba 100644 --- a/vnet/vnet/buffer.h +++ b/vnet/vnet/buffer.h @@ -143,7 +143,11 @@ typedef struct u8 code; u32 data; } icmp; + + /* IP header - saved by ip*_local nodes */ + void *header; }; + } ip; /* diff --git a/vnet/vnet/ip/ip4_forward.c b/vnet/vnet/ip/ip4_forward.c index fc7b3496..38729c8e 100644 --- a/vnet/vnet/ip/ip4_forward.c +++ b/vnet/vnet/ip/ip4_forward.c @@ -1472,9 +1472,12 @@ ip4_local (vlib_main_t * vm, ip0 = vlib_buffer_get_current (p0); ip1 = vlib_buffer_get_current (p1); - fib_index0 = vec_elt (im->fib_index_by_sw_if_index, - vnet_buffer(p0)->sw_if_index[VLIB_RX]); - fib_index1 = vec_elt (im->fib_index_by_sw_if_index, + vnet_buffer (p0)->ip.header = ip0; + vnet_buffer (p1)->ip.header = ip1; + + fib_index0 = vec_elt (im->fib_index_by_sw_if_index, + vnet_buffer (p0)->sw_if_index[VLIB_RX]); + fib_index1 = vec_elt (im->fib_index_by_sw_if_index, vnet_buffer(p1)->sw_if_index[VLIB_RX]); mtrie0 = &ip4_fib_get (fib_index0)->mtrie; @@ -1679,6 +1682,8 @@ ip4_local (vlib_main_t * vm, ip0 = vlib_buffer_get_current (p0); + vnet_buffer (p0)->ip.header = ip0; + fib_index0 = vec_elt (im->fib_index_by_sw_if_index, vnet_buffer(p0)->sw_if_index[VLIB_RX]); @@ -3294,4 +3299,3 @@ VLIB_CLI_COMMAND (set_ip_classify_command, static) = { .function = set_ip_classify_command_fn, }; /* *INDENT-ON* */ - diff --git a/vnet/vnet/ip/udp.h b/vnet/vnet/ip/udp.h index 4de30f1d..f8ff777e 100644 --- a/vnet/vnet/ip/udp.h +++ b/vnet/vnet/ip/udp.h @@ -37,6 +37,7 @@ typedef enum { _ (67, dhcp_to_server) \ _ (68, dhcp_to_client) \ _ (500, ikev2) \ +_ (3784, bfd4) \ _ (4341, lisp_gpe) \ _ (4342, lisp_cp) \ _ (4739, ipfix) \ @@ -49,6 +50,7 @@ _ (6633, vpath_3) #define foreach_udp6_dst_port \ _ (547, dhcpv6_to_server) \ _ (546, dhcpv6_to_client) \ +_ (3784, bfd6) \ _ (4341, lisp_gpe6) \ _ (4342, lisp_cp6) \ _ (4790, vxlan6_gpe) \ diff --git a/vpp/vpp-api/api.c b/vpp/vpp-api/api.c index b7753092..a5ac060a 100644 --- a/vpp/vpp-api/api.c +++ b/vpp/vpp-api/api.c @@ -117,6 +117,8 @@ #include #include +#include +#include #include #include #include @@ -302,7 +304,12 @@ _(PUNT, punt) \ _(FLOW_CLASSIFY_SET_INTERFACE, flow_classify_set_interface) \ _(FLOW_CLASSIFY_DUMP, flow_classify_dump) \ _(IPSEC_SPD_DUMP, ipsec_spd_dump) \ -_(FEATURE_ENABLE_DISABLE, feature_enable_disable) +_(FEATURE_ENABLE_DISABLE, feature_enable_disable) \ +_(BFD_UDP_ADD, bfd_udp_add) \ +_(BFD_UDP_DEL, bfd_udp_del) \ +_(BFD_UDP_SESSION_DUMP, bfd_udp_session_dump) \ +_(BFD_SESSION_SET_FLAGS, bfd_session_set_flags) \ +_(WANT_BFD_EVENTS, want_bfd_events) #define QUOTE_(x) #x #define QUOTE(x) QUOTE_(x) @@ -343,6 +350,7 @@ vl_api_memclnt_delete_callback (u32 client_index) } pub_sub_handler (oam_events, OAM_EVENTS); +pub_sub_handler (bfd_events, BFD_EVENTS); #define RESOLUTION_EVENT 1 #define RESOLUTION_PENDING_EVENT 2 @@ -6766,6 +6774,163 @@ static void BAD_SW_IF_INDEX_LABEL; REPLY_MACRO (VL_API_L2_INTERFACE_PBB_TAG_REWRITE_REPLY); + +} + +static void +vl_api_bfd_udp_add_t_handler (vl_api_bfd_udp_add_t * mp) +{ + vl_api_bfd_udp_add_reply_t *rmp; + int rv; + + VALIDATE_SW_IF_INDEX (mp); + + ip46_address_t local_addr; + memset (&local_addr, 0, sizeof (local_addr)); + ip46_address_t peer_addr; + memset (&peer_addr, 0, sizeof (peer_addr)); + if (mp->is_ipv6) + { + clib_memcpy (&local_addr.ip6, mp->local_addr, sizeof (local_addr.ip6)); + clib_memcpy (&peer_addr.ip6, mp->peer_addr, sizeof (peer_addr.ip6)); + } + else + { + clib_memcpy (&local_addr.ip4, mp->local_addr, sizeof (local_addr.ip4)); + clib_memcpy (&peer_addr.ip4, mp->peer_addr, sizeof (peer_addr.ip4)); + } + + rv = bfd_udp_add_session (clib_net_to_host_u32 (mp->sw_if_index), + clib_net_to_host_u32 (mp->desired_min_tx), + clib_net_to_host_u32 (mp->required_min_rx), + mp->detect_mult, &local_addr, &peer_addr); + + BAD_SW_IF_INDEX_LABEL; + REPLY_MACRO (VL_API_BFD_UDP_ADD_REPLY); +} + +static void +vl_api_bfd_udp_del_t_handler (vl_api_bfd_udp_del_t * mp) +{ + vl_api_bfd_udp_del_reply_t *rmp; + int rv; + + VALIDATE_SW_IF_INDEX (mp); + + ip46_address_t local_addr; + memset (&local_addr, 0, sizeof (local_addr)); + ip46_address_t peer_addr; + memset (&peer_addr, 0, sizeof (peer_addr)); + if (mp->is_ipv6) + { + clib_memcpy (&local_addr.ip6, mp->local_addr, sizeof (local_addr.ip6)); + clib_memcpy (&peer_addr.ip6, mp->peer_addr, sizeof (peer_addr.ip6)); + } + else + { + clib_memcpy (&local_addr.ip4, mp->local_addr, sizeof (local_addr.ip4)); + clib_memcpy (&peer_addr.ip4, mp->peer_addr, sizeof (peer_addr.ip4)); + } + + rv = + bfd_udp_del_session (clib_net_to_host_u32 (mp->sw_if_index), &local_addr, + &peer_addr); + + BAD_SW_IF_INDEX_LABEL; + REPLY_MACRO (VL_API_BFD_UDP_DEL_REPLY); +} + +void +send_bfd_udp_session_details (unix_shared_memory_queue_t * q, u32 context, + bfd_session_t * bs) +{ + if (bs->transport != BFD_TRANSPORT_UDP4 && + bs->transport != BFD_TRANSPORT_UDP6) + { + return; + } + + vl_api_bfd_udp_session_details_t *mp = vl_msg_api_alloc (sizeof (*mp)); + memset (mp, 0, sizeof (*mp)); + mp->_vl_msg_id = ntohs (VL_API_BFD_UDP_SESSION_DETAILS); + mp->context = context; + mp->bs_index = clib_host_to_net_u32 (bs->bs_idx); + mp->state = bs->local_state; + bfd_udp_session_t *bus = &bs->udp; + bfd_udp_key_t *key = &bus->key; + mp->sw_if_index = clib_host_to_net_u32 (key->sw_if_index); + mp->is_ipv6 = !(ip46_address_is_ip4 (&key->local_addr)); + if (mp->is_ipv6) + { + clib_memcpy (mp->local_addr, &key->local_addr, + sizeof (key->local_addr)); + clib_memcpy (mp->peer_addr, &key->peer_addr, sizeof (key->peer_addr)); + } + else + { + clib_memcpy (mp->local_addr, &key->local_addr.ip4.data, + sizeof (&key->local_addr.ip4.data)); + clib_memcpy (mp->peer_addr, &key->peer_addr.ip4.data, + sizeof (&key->peer_addr.ip4.data)); + } + + vl_msg_api_send_shmem (q, (u8 *) & mp); +} + +void +bfd_event (bfd_main_t * bm, bfd_session_t * bs) +{ + vpe_api_main_t *vam = &vpe_api_main; + vpe_client_registration_t *reg; + unix_shared_memory_queue_t *q; + /* *INDENT-OFF* */ + pool_foreach (reg, vam->bfd_events_registrations, ({ + q = vl_api_client_index_to_input_queue (reg->client_index); + if (q) + { + switch (bs->transport) + { + case BFD_TRANSPORT_UDP4: + /* fallthrough */ + case BFD_TRANSPORT_UDP6: + send_bfd_udp_session_details (q, 0, bs); + } + } + })); + /* *INDENT-ON* */ +} + +static void +vl_api_bfd_udp_session_dump_t_handler (vl_api_bfd_udp_session_dump_t * mp) +{ + unix_shared_memory_queue_t *q; + + q = vl_api_client_index_to_input_queue (mp->client_index); + + if (q == 0) + return; + + bfd_session_t *bs = NULL; + /* *INDENT-OFF* */ + pool_foreach (bs, bfd_main.sessions, ({ + if (bs->transport == BFD_TRANSPORT_UDP4 || + bs->transport == BFD_TRANSPORT_UDP6) + send_bfd_udp_session_details (q, mp->context, bs); + })); + /* *INDENT-ON* */ +} + +static void +vl_api_bfd_session_set_flags_t_handler (vl_api_bfd_session_set_flags_t * mp) +{ + vl_api_bfd_session_set_flags_reply_t *rmp; + int rv; + + rv = + bfd_session_set_flags (clib_net_to_host_u32 (mp->bs_index), + mp->admin_up_down); + + REPLY_MACRO (VL_API_BFD_SESSION_SET_FLAGS_REPLY); } static void @@ -7090,6 +7255,7 @@ vpe_api_init (vlib_main_t * vm) am->to_netconf_client_registration_hash = hash_create (0, sizeof (uword)); am->from_netconf_client_registration_hash = hash_create (0, sizeof (uword)); am->oam_events_registration_hash = hash_create (0, sizeof (uword)); + am->bfd_events_registration_hash = hash_create (0, sizeof (uword)); vl_api_init (vm); vl_set_memory_region_name ("/vpe-api"); diff --git a/vpp/vpp-api/vpe.api b/vpp/vpp-api/vpe.api index 1c33f70b..b13c2606 100644 --- a/vpp/vpp-api/vpe.api +++ b/vpp/vpp-api/vpe.api @@ -4656,6 +4656,191 @@ define feature_enable_disable_reply i32 retval; }; +/** \brief Configure BFD feature + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param slow_timer - slow timer (seconds) + @param min_tx - desired min tx interval + @param min_rx - desired min rx interval + @param detect_mult - desired detection multiplier +*/ +define bfd_set_config { + u32 client_index; + u32 context; + u32 slow_timer; + u32 min_tx; + u32 min_rx; + u8 detect_mult; +}; + +/** \brief Configure BFD feature response + @param context - sender context, to match reply w/ request + @param retval - return code for the request +*/ +define bfd_set_config_reply { + u32 context; + i32 retval; +}; + +/** \brief Get BFD configuration +*/ +define bfd_get_config { + u32 client_index; + u32 context; +}; + +/** \brief Get BFD configuration response + @param context - sender context, to match reply w/ request + @param retval - return code for the request + @param slow_timer - slow timer (seconds) + @param min_tx - desired min tx interval + @param min_rx - desired min rx interval + @param detect_mult - desired detection multiplier +*/ +define bfd_get_config_reply { + u32 client_index; + u32 context; + u32 slow_timer; + u32 min_tx; + u32 min_rx; + u8 detect_mult; +}; + +/** \brief Add UDP BFD session on interface + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param sw_if_index - sw index of the interface + @param desired_min_tx - desired min transmit interval (microseconds) + @param required_min_rx - required min receive interval (microseconds) + @param detect_mult - detect multiplier (# of packets missed between connection goes down) + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 +*/ +define bfd_udp_add { + u32 client_index; + u32 context; + u32 sw_if_index; + u32 desired_min_tx; + u32 required_min_rx; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; + u8 detect_mult; +}; + +/** \brief Add UDP BFD session response + @param context - sender context, to match reply w/ request + @param retval - return code for the request + @param bs_index - index of the session created +*/ +define bfd_udp_add_reply { + u32 context; + i32 retval; + u32 bs_index; +}; + +/** \brief Delete UDP BFD session on interface + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param sw_if_index - sw index of the interface + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 +*/ +define bfd_udp_del { + u32 client_index; + u32 context; + u32 sw_if_index; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; +}; + +/** \brief Delete UDP BFD session response + @param context - sender context, to match reply w/ request + @param retval - return code for the request +*/ +define bfd_udp_del_reply { + u32 context; + i32 retval; +}; + +/** \brief Get all BFD sessions + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request +*/ +define bfd_udp_session_dump { + u32 client_index; + u32 context; +}; + +/** \brief BFD session details structure + @param context - sender context, to match reply w/ request + @param bs_index - index of the session + @param sw_if_index - sw index of the interface + @param local_addr - local address + @param peer_addr - peer address + @param is_ipv6 - local_addr, peer_addr are IPv6 if non-zero, otherwise IPv4 + @param state - session state +*/ +define bfd_udp_session_details { + u32 context; + u32 bs_index; + u32 sw_if_index; + u8 local_addr[16]; + u8 peer_addr[16]; + u8 is_ipv6; + u8 state; +}; + +/** \brief Set flags of BFD session + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param bs_index - index of the bfd session to set flags on + @param admin_up_down - set the admin state, 1 = up, 0 = down +*/ +define bfd_session_set_flags { + u32 client_index; + u32 context; + u32 bs_index; + u8 admin_up_down; +}; + +/** \brief Reply to bfd_session_set_flags + @param context - sender context which was passed in the request + @param retval - return code of the set flags request +*/ +define bfd_session_set_flags_reply +{ + u32 context; + i32 retval; +}; + +/** \brief Register for BFD events + @param client_index - opaque cookie to identify the sender + @param context - sender context, to match reply w/ request + @param enable_disable - 1 => register for events, 0 => cancel registration + @param pid - sender's pid +*/ +define want_bfd_events +{ + u32 client_index; + u32 context; + u32 enable_disable; + u32 pid; +}; + +/** \brief Reply for BFD events registration + @param context - returned sender context, to match reply w/ request + @param retval - return code +*/ +define want_bfd_events_reply +{ + u32 context; + i32 retval; +}; + /* * Local Variables: * eval: (c-set-style "gnu") diff --git a/vppinfra/vppinfra/mhash.c b/vppinfra/vppinfra/mhash.c index 84517e19..c917e164 100644 --- a/vppinfra/vppinfra/mhash.c +++ b/vppinfra/vppinfra/mhash.c @@ -223,7 +223,7 @@ mhash_init (mhash_t * h, uword n_value_bytes, uword n_key_bytes) } static uword -mhash_set_tmp_key (mhash_t * h, void *key) +mhash_set_tmp_key (mhash_t * h, const void *key) { u8 *key_tmp; int my_cpu = os_get_cpu_number (); @@ -251,7 +251,7 @@ mhash_set_tmp_key (mhash_t * h, void *key) } hash_pair_t * -mhash_get_pair (mhash_t * h, void *key) +mhash_get_pair (mhash_t * h, const void *key) { uword ikey; mhash_sanitize_hash_user (h); diff --git a/vppinfra/vppinfra/mhash.h b/vppinfra/vppinfra/mhash.h index 8ce8454b..102adf4e 100644 --- a/vppinfra/vppinfra/mhash.h +++ b/vppinfra/vppinfra/mhash.h @@ -101,13 +101,13 @@ mhash_key_to_mem (mhash_t * h, uword key) return vec_elt_at_index (h->key_vector_or_heap, key); } -hash_pair_t *mhash_get_pair (mhash_t * h, void *key); +hash_pair_t *mhash_get_pair (mhash_t * h, const void *key); uword mhash_set_mem (mhash_t * h, void *key, uword * new_value, uword * old_value); uword mhash_unset (mhash_t * h, void *key, uword * old_value); always_inline uword * -mhash_get (mhash_t * h, void *key) +mhash_get (mhash_t * h, const void *key) { hash_pair_t *p = mhash_get_pair (h, key); return p ? &p->value[0] : 0; -- cgit 1.2.3-korg From 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/hook.py') 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 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/hook.py') 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