From 7ea7ab5f215a95dbc1a38acc03b7fea6d3dbedcf Mon Sep 17 00:00:00 2001 From: Naveen Joy Date: Tue, 11 May 2021 10:31:18 -0700 Subject: [PATCH] tests: run a test inside a QEMU VM Use the script test/run.py to run a test named test_vm_tap inside a QEMU VM. The run script builds out a virtual env, launches a light weight QEMU VM, mounts host directories, starts VPP inside the VM and runs the test. The test named test_vm_tap, creates two tap v2 interfaces in separate Linux namespaces and using iPerf, streams traffic between the VM and VPP. All data files are stored in the directory named /tmp/vpp-vm-tests. To clean up, use the make test-wipe command. Usage: test/run.py --vm --debug --test test_vm_tap Type: improvement Change-Id: I4425dbef52acee1e5b8af5acaa169b89a2c0f171 Signed-off-by: Naveen Joy --- test/Makefile | 1 + test/run.py | 370 ++++++++++++++++++++++++++++++++++++++++++ test/scripts/run_vpp_in_vm.sh | 198 ++++++++++++++++++++++ test/test_vm_tap.py | 102 ++++++++++++ test/vpp_iperf.py | 118 ++++++++++++++ test/vpp_qemu_utils.py | 20 +++ 6 files changed, 809 insertions(+) create mode 100755 test/run.py create mode 100755 test/scripts/run_vpp_in_vm.sh create mode 100644 test/test_vm_tap.py create mode 100644 test/vpp_iperf.py create mode 100644 test/vpp_qemu_utils.py diff --git a/test/Makefile b/test/Makefile index 08c6eabe100..e5c2b2d21d5 100644 --- a/test/Makefile +++ b/test/Makefile @@ -310,6 +310,7 @@ reset: @if [ $(FORCE_NO_WIPE) -eq "0" ] ; then rm -rf /tmp/vpp-unittest-*; fi @rm -f /tmp/api_post_mortem.* @rm -rf $(FAILED_DIR) + @rm -rf /tmp/vpp-vm-tests .PHONY: wipe wipe: reset diff --git a/test/run.py b/test/run.py new file mode 100755 index 00000000000..07b24d55b82 --- /dev/null +++ b/test/run.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022 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. +# +# Build the Virtual Environment & run VPP unit tests + +import argparse +import glob +import logging +import os +from pathlib import Path +import signal +from subprocess import Popen, PIPE, STDOUT +import sys +import time +import venv + + +# Required Std. Path Variables +test_dir = os.path.dirname(os.path.realpath(__file__)) +ws_root = os.path.dirname(test_dir) +build_root = os.path.join(ws_root, "build-root") +venv_dir = os.path.join(test_dir, "venv") +venv_bin_dir = os.path.join(venv_dir, "bin") +venv_lib_dir = os.path.join(venv_dir, "lib") +venv_run_dir = os.path.join(venv_dir, "run") +venv_install_done = os.path.join(venv_run_dir, "venv_install.done") +papi_python_src_dir = os.path.join(ws_root, "src", "vpp-api", "python") + +# Path Variables Set after VPP Build/Install +vpp_build_dir = vpp_install_path = vpp_bin = vpp_lib = vpp_lib64 = None +vpp_plugin_path = vpp_test_plugin_path = ld_library_path = None + +# Pip version pinning +pip_version = "22.0.4" +pip_tools_version = "6.6.0" + +# Test requirement files +test_requirements_file = os.path.join(test_dir, "requirements.txt") +# Auto-generated requirement file +pip_compiled_requirements_file = os.path.join(test_dir, "requirements-3.txt") + + +# Gracefully exit after executing cleanup scripts +# upon receiving a SIGINT or SIGTERM +def handler(signum, frame): + print("Received Signal {0}".format(signum)) + post_vm_test_run() + + +signal.signal(signal.SIGINT, handler) +signal.signal(signal.SIGTERM, handler) + + +def show_progress(stream): + """ + Read lines from a subprocess stdout/stderr streams and write + to sys.stdout & the logfile + """ + while True: + s = stream.readline() + if not s: + break + data = s.decode("utf-8") + # Filter the annoying SIGTERM signal from the output when VPP is + # terminated after a test run + if "SIGTERM" not in data: + sys.stdout.write(data) + logging.debug(data) + sys.stdout.flush() + stream.close() + + +class ExtendedEnvBuilder(venv.EnvBuilder): + """ + 1. Builds a Virtual Environment for running VPP unit tests + 2. Installs all necessary scripts, pkgs & patches into the vEnv + - python3, pip, pip-tools, papi, scapy patches & + test-requirement pkgs + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def post_setup(self, context): + """ + Setup all packages that need to be pre-installed into the venv + prior to running VPP unit tests. + + :param context: The context of the virtual environment creation + request being processed. + """ + os.environ["VIRTUAL_ENV"] = context.env_dir + os.environ[ + "CUSTOM_COMPILE_COMMAND" + ] = "make test-refresh-deps (or update requirements.txt)" + # Cleanup previously auto-generated pip req. file + try: + os.unlink(pip_compiled_requirements_file) + except OSError: + pass + # Set the venv python executable & binary install path + env_exe = context.env_exe + bin_path = context.bin_path + # Packages/requirements to be installed in the venv + # [python-module, cmdline-args, package-name_or_requirements-file-name] + test_req = [ + ["pip", "install", "pip===%s" % pip_version], + ["pip", "install", "pip-tools===%s" % pip_tools_version], + [ + "piptools", + "compile", + "-q", + "--generate-hashes", + test_requirements_file, + "--output-file", + pip_compiled_requirements_file, + ], + ["piptools", "sync", pip_compiled_requirements_file], + ["pip", "install", "-e", papi_python_src_dir], + ] + for req in test_req: + args = [env_exe, "-m"] + args.extend(req) + print(args) + p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=bin_path) + show_progress(p.stdout) + self.pip_patch() + + def pip_patch(self): + """ + Apply scapy patch files + """ + scapy_patch_dir = Path(os.path.join(test_dir, "patches", "scapy-2.4.3")) + scapy_source_dir = glob.glob( + os.path.join(venv_lib_dir, "python3.*", "site-packages") + )[0] + for f in scapy_patch_dir.iterdir(): + print("Applying patch: {}".format(os.path.basename(str(f)))) + args = ["patch", "--forward", "-p1", "-d", scapy_source_dir, "-i", str(f)] + print(args) + p = Popen(args, stdout=PIPE, stderr=STDOUT) + show_progress(p.stdout) + + +# Build VPP Release/Debug binaries +def build_vpp(debug=True, release=False): + """ + Install VPP Release(if release=True) or Debug(if debug=True) Binaries. + + Default is to build the debug binaries. + """ + global vpp_build_dir, vpp_install_path, vpp_bin, vpp_lib, vpp_lib64 + global vpp_plugin_path, vpp_test_plugin_path, ld_library_path + if debug: + print("Building VPP debug binaries") + args = ["make", "build"] + build = "build-vpp_debug-native" + install = "install-vpp_debug-native" + elif release: + print("Building VPP release binaries") + args = ["make", "build-release"] + build = "build-vpp-native" + install = "install-vpp-native" + p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=ws_root) + show_progress(p.stdout) + vpp_build_dir = os.path.join(build_root, build) + vpp_install_path = os.path.join(build_root, install) + vpp_bin = os.path.join(vpp_install_path, "vpp", "bin", "vpp") + vpp_lib = os.path.join(vpp_install_path, "vpp", "lib") + vpp_lib64 = os.path.join(vpp_install_path, "vpp", "lib64") + vpp_plugin_path = ( + os.path.join(vpp_lib, "vpp_plugins") + + ":" + + os.path.join(vpp_lib64, "vpp_plugins") + ) + vpp_test_plugin_path = ( + os.path.join(vpp_lib, "vpp_api_test_plugins") + + ":" + + os.path.join(vpp_lib64, "vpp_api_test_plugins") + ) + ld_library_path = os.path.join(vpp_lib) + ":" + os.path.join(vpp_lib64) + + +# Environment Vars required by the test framework, +# papi_provider & unittests +def set_environ(): + os.environ["WS_ROOT"] = ws_root + os.environ["BR"] = build_root + os.environ["VENV_PATH"] = venv_dir + os.environ["VENV_BIN"] = venv_bin_dir + os.environ["RND_SEED"] = str(time.time()) + os.environ["VPP_BUILD_DIR"] = vpp_build_dir + os.environ["VPP_BIN"] = vpp_bin + os.environ["VPP_PLUGIN_PATH"] = vpp_plugin_path + os.environ["VPP_TEST_PLUGIN_PATH"] = vpp_test_plugin_path + os.environ["VPP_INSTALL_PATH"] = vpp_install_path + os.environ["LD_LIBRARY_PATH"] = ld_library_path + os.environ["FAILED_DIR"] = "/tmp/vpp-failed-unittests/" + if not os.environ.get("TEST_JOBS"): + os.environ["TEST_JOBS"] = "1" + + +# Runs a test inside a spawned QEMU VM +# If a kernel image is not provided, a linux-image-kvm image is +# downloaded to the test_data_dir +def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem): + script = os.path.join(test_dir, "scripts", "run_vpp_in_vm.sh") + p = Popen( + [script, test_name, kernel_image, test_data_dir, cpu_mask, mem], + stdout=PIPE, + stderr=STDOUT, + cwd=ws_root, + ) + show_progress(p.stdout) + post_vm_test_run() + + +def post_vm_test_run(): + # Revert the ownership of certain directories from root to the + # original user after running in QEMU + print("Running post test cleanup tasks") + dirs = ["/tmp/vpp-failed-unittests", os.path.join(ws_root, "test", "__pycache__")] + dirs.extend(glob.glob("/tmp/vpp-unittest-*")) + dirs.extend(glob.glob("/tmp/api_post_mortem.*")) + user = os.getlogin() + for dir in dirs: + if os.path.exists(dir) and Path(dir).owner() != user: + cmd = ["sudo", "chown", "-R", "{0}:{0}".format(user), dir] + p = Popen(cmd, stdout=PIPE, stderr=STDOUT) + show_progress(p.stdout) + + +def build_venv(): + # Builds a virtual env containing all the required packages and patches + # for running VPP unit tests + if not os.path.exists(venv_install_done): + env_builder = ExtendedEnvBuilder(clear=True, with_pip=True) + print("Creating a vEnv for running VPP unit tests in {}".format(venv_dir)) + env_builder.create(venv_dir) + # Write state to the venv run dir + Path(venv_run_dir).mkdir(exist_ok=True) + Path(venv_install_done).touch() + + +def expand_mix_string(s): + # Returns an expanded string computed from a mixrange string (s) + # E.g: If param s = '5-8,10,11' returns '5,6,7,8,10,11' + result = [] + for val in s.split(","): + if "-" in val: + start, end = val.split("-") + result.extend(list(range(int(start), int(end) + 1))) + else: + result.append(int(val)) + return ",".join(str(i) for i in set(result)) + + +def set_logging(test_data_dir, test_name): + Path(test_data_dir).mkdir(exist_ok=True) + log_file = "vm_{0}_{1}.log".format(test_name, str(time.time())[-5:]) + filename = "{0}/{1}".format(test_data_dir, log_file) + Path(filename).touch() + logging.basicConfig(filename=filename, level=logging.DEBUG) + + +if __name__ == "__main__": + # Build a Virtual Environment for running tests on host & QEMU + parser = argparse.ArgumentParser( + description="Run VPP Unit Tests", formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--vm", + dest="vm", + required=True, + action="store_true", + help="Run Test Inside a QEMU VM", + ) + parser.add_argument( + "-d", + "--debug", + dest="debug", + required=False, + default=True, + action="store_true", + help="Run Tests on Debug Build", + ) + parser.add_argument( + "-r", + "--release", + dest="release", + required=False, + default=False, + action="store_true", + help="Run Tests on release Build", + ) + parser.add_argument( + "--test", + dest="test_name", + required=False, + action="store", + default="", + help="Tests to Run", + ) + parser.add_argument( + "--vm-kernel-image", + dest="kernel_image", + required=False, + action="store", + default="", + help="Kernel Image Selection to Boot", + ) + parser.add_argument( + "--vm-cpu-list", + dest="vm_cpu_list", + required=False, + action="store", + default="5-8", + help="Set CPU Affinity\n" + "E.g. 5-7,10 will schedule on processors " + "#5, #6, #7 and #10. (Default: 5-8)", + ) + parser.add_argument( + "--vm-mem", + dest="vm_mem", + required=False, + action="store", + default="2", + help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)", + ) + args = parser.parse_args() + # Enable VM tests + if args.vm and args.test_name: + test_data_dir = "/tmp/vpp-vm-tests" + set_logging(test_data_dir, args.test_name) + vm_tests = True + elif args.vm and not args.test_name: + print("Error: The --test argument must be set for running VM tests") + sys.exit(1) + build_venv() + # Build VPP release or debug binaries + debug = False if args.release else True + build_vpp(debug, args.release) + set_environ() + if vm_tests: + print("Running VPP unit test(s):{0} inside a QEMU VM".format(args.test_name)) + # Check Available CPUs & Usable Memory + cpus = expand_mix_string(args.vm_cpu_list) + num_cpus, usable_cpus = (len(cpus.split(",")), len(os.sched_getaffinity(0))) + if num_cpus > usable_cpus: + print(f"Error:# of CPUs:{num_cpus} > Avail CPUs:{usable_cpus}") + sys.exit(1) + avail_mem = int(os.popen("free -t -g").readlines()[-1].split()[-1]) + if int(args.vm_mem) > avail_mem: + print(f"Error: Mem Size:{args.vm_mem}G > Avail Mem:{avail_mem}G") + sys.exit(1) + vm_test_runner( + args.test_name, args.kernel_image, test_data_dir, cpus, f"{args.vm_mem}G" + ) diff --git a/test/scripts/run_vpp_in_vm.sh b/test/scripts/run_vpp_in_vm.sh new file mode 100755 index 00000000000..8b8d14e9af3 --- /dev/null +++ b/test/scripts/run_vpp_in_vm.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# Run VPP in a QEMU VM +# set -o xtrace +set -o nounset + +# Arguments: +# $1:- Test Filter +# $2:- Kernel Image +# $3:- Test Data Directory +# $4:- CPU Mask String (e.g. "5,6,7,8") +# $5:- Guest MEM in Gibibytes (e.g. 2G) + +if [[ -z "${1:-}" ]]; then + echo "ERROR: A non-empty test selection is required to run + tests in a QEMU VM" + exit 1 +fi +TEST=${1:-} +TEST_JOBS=${TEST_JOBS:-1} + +# Init RAM disk image to boot the QEMU VM +INITRD=${INITRD:-} + +# Ensure test dir +TEST_DATA_DIR=${3:-"/tmp/vpp-vm-tests"} +if [[ ! -d ${TEST_DATA_DIR} ]]; then + mkdir -p ${TEST_DATA_DIR} +fi + +# CPU Affinity for taskset +CPU_MASK=${4:-"5,6,7,8"} +IFS=',' read -r -a CPU_MASK_ARRAY <<< ${CPU_MASK} +CPUS=${#CPU_MASK_ARRAY[@]} + +# Guest MEM (Default 2G) +MEM=${5:-"2G"} + +# Set the QEMU executable for the OS pkg. +os_VENDOR=$(lsb_release -i -s) +if [[ $os_VENDOR =~ (Debian|Ubuntu) ]]; then + os_PACKAGE="deb" + QEMU=${QEMU:-"qemu-system-x86_64"} +else + os_PACKAGE="rpm" + QEMU=${QEMU:-"qemu-kvm"} +fi + +# Exit if the ${QEMU} executable is not available +if ! command -v ${QEMU} &> /dev/null; then + echo "Error: ${QEMU} is required, but could not be found." + exit 1 +fi + +# Download the Generic Linux Kernel, if needed +if [[ -z "${2:-}" ]] || [[ ! -f "${2:-}" ]]; then + if [[ $os_PACKAGE == "deb" ]]; then + PWD=$(pwd) + cd ${TEST_DATA_DIR} + PKG="linux-image-$(uname -r)" + echo "Getting the Linux Kernel image..${PKG}" + apt-get download ${PKG} + dpkg --fsys-tarfile ${PKG}_*.deb | tar xvf - ./boot + KERNEL_BIN=$(ls ${TEST_DATA_DIR}/boot/vmlinuz-*-generic) + cd ${PWD} + else + echo "ERROR: Kernel Image selection is required for RPM pkgs." + exit 1 + fi +else + KERNEL_BIN=${2:-} +fi + +## Create initrd with 9p drivers, if ${INITRD} is null +DRIVERS_9P="" +if [[ -z "${INITRD}" ]] && [[ ! -d "/etc/initramfs-tools" ]]; then + echo "To boot the QEMU VM, an initial RAM disk with 9p drivers is needed" + echo "Install the initramfs-tools package or set env var INITRD to the RAM disk path" + exit 1 +elif [[ -z "${INITRD}" ]]; then + if [[ -f "/etc/initramfs-tools/modules" ]]; then + DRIVERS_9P=$(grep 9p /etc/initramfs-tools/modules | awk '{print $1}' | cut -d$'\n' -f1) + fi + if [[ -z "${DRIVERS_9P}" ]]; then + echo "You'll need to update the file /etc/initramfs-tools/modules with the below 9p drivers" + echo "9p >> /etc/initramfs-tools/modules" + echo "9pnet >> /etc/initramfs-tools/modules" + echo "9pnet_virtio >> /etc/initramfs-tools/modules" + exit 1 + fi + # Generate the initramfs image, if the we haven't generated one yet + if ! ls ${TEST_DATA_DIR}/boot/initrd.img-*-generic &> /dev/null; then + echo "Generating a bootable initramfs image in ${TEST_DATA_DIR}/boot/" + update-initramfs -c -k $(uname -r) -b ${TEST_DATA_DIR}/boot >/dev/null 2>&1 + echo "Generated the INITRD image" + fi + INITRD=$(ls ${TEST_DATA_DIR}/boot/initrd.img-*-generic) +fi +echo "Using INITRD=${TEST_DATA_DIR}/boot/${INITRD} for booting the QEMU VM" + + +## Install iperf into ${TEST_DATA_DIR} +IPERF=${TEST_DATA_DIR}/usr/bin/iperf +if [[ ! -x ${IPERF} ]] && [[ $os_PACKAGE == "deb" ]]; then + echo "Installing iperf: ${IPERF}" + PWD=$(pwd) + cd ${TEST_DATA_DIR} + IPRF_PKG="iperf_2.0.5+dfsg1-2_amd64.deb" + wget https://iperf.fr/download/ubuntu/${IPRF_PKG} + dpkg --fsys-tarfile ${IPRF_PKG} | tar xvf - + if [[ -x ${IPERF} ]]; then + echo "${IPERF} installed successfully" + else + echo "ERROR: iperf executable ${IPERF} installation failed" + exit 1 + fi + cd ${PWD} +elif [[ ! -x ${IPERF} ]] && [[ $os_PACKAGE != "deb" ]]; then + echo "ERROR: install iperf: ${IPERF} before running QEMU tests" + exit 1 +fi + +FAILED_DIR=${FAILED_DIR:-"/tmp/vpp-failed-unittests/"} +if [[ ! -d ${FAILED_DIR} ]]; then + mkdir -p ${FAILED_DIR} +fi + +HUGEPAGES=${HUGEPAGES:-256} + +# Ensure all required Env vars are bound to non-zero values +EnvVarArray=("WS_ROOT=${WS_ROOT:-}" + "RND_SEED=${RND_SEED:-}" + "BR=${BR:-}" + "VENV_PATH=${VENV_PATH:-}" + "VPP_BUILD_DIR=${VPP_BUILD_DIR:-}" + "VPP_BIN=${VPP_BIN:-}" + "VPP_PLUGIN_PATH=${VPP_PLUGIN_PATH:-}" + "VPP_TEST_PLUGIN_PATH=${VPP_TEST_PLUGIN_PATH:-}" + "VPP_INSTALL_PATH=${VPP_INSTALL_PATH:-}" + "LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}") + +for envVar in ${EnvVarArray[*]}; do + var_name=$(echo $envVar | cut -d= -f1) + var_val=$(echo $envVar | cut -d= -f2) + if [[ -z "$var_val" ]]; then + echo "ERROR: Env var: $var_name is not set" + exit 1 + fi +done + +# Boot QEMU VM and run the test +function run_in_vm { + INIT=$(mktemp -p ${TEST_DATA_DIR}) + cat > ${INIT} << _EOF_ +#!/bin/bash +mkdir -p /dev/shm +mount -t tmpfs -o rw,nosuid,nodev tmpfs /dev/shm +mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true +mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run +mount -t 9p /dev/vpp9p ${WS_ROOT} +mount -t 9p tmp9p /tmp +modprobe -a vhost_net +env SOCKET=1 SANITY=no \ +FAILED_DIR=${FAILED_DIR} RND_SEED=${RND_SEED} BR=${BR} \ +VENV_PATH=${VENV_PATH} TEST=${TEST} TEST_JOBS=${TEST_JOBS} \ +VPP_BUILD_DIR=${VPP_BUILD_DIR} VPP_BIN=${VPP_BIN} VPP_PLUGIN_PATH=${VPP_PLUGIN_PATH} \ +VPP_TEST_PLUGIN_PATH=${VPP_TEST_PLUGIN_PATH} VPP_INSTALL_PATH=${VPP_INSTALL_PATH} \ +LD_LIBRARY_PATH=${LD_LIBRARY_PATH} TEST_DATA_DIR=${TEST_DATA_DIR} INITRD=${INITRD} \ +bash -c "${WS_ROOT}/test/scripts/run.sh --filter=${TEST} --jobs=${TEST_JOBS} --failed-dir=${FAILED_DIR} \ +--venv-dir=${VENV_PATH} --vpp-ws-dir=${WS_ROOT} --extended" +poweroff -f +_EOF_ + + chmod +x ${INIT} + + sudo taskset -c ${CPU_MASK} ${QEMU} \ + -nodefaults \ + -name test_$(basename $INIT) \ + -chardev stdio,mux=on,id=char0 \ + -mon chardev=char0,mode=readline,pretty=on \ + -serial chardev:char0 \ + -machine pc,accel=kvm,usb=off,mem-merge=off \ + -cpu host \ + -smp ${CPUS},sockets=1,cores=${CPUS},threads=1 \ + -m ${MEM} \ + -no-user-config \ + -kernel ${KERNEL_BIN} \ + -initrd ${INITRD} \ + -fsdev local,id=root9p,path=/,security_model=none,multidevs=remap \ + -device virtio-9p-pci,fsdev=root9p,mount_tag=fsRoot \ + -virtfs local,path=${WS_ROOT},mount_tag=/dev/vpp9p,security_model=none,id=vpp9p,multidevs=remap \ + -virtfs local,path=/tmp,mount_tag=tmp9p,security_model=passthrough,id=tmp9p,multidevs=remap \ + -netdev tap,id=net0,vhost=on \ + -device virtio-net-pci,netdev=net0,mac=52:54:00:de:64:01 \ + -nographic \ + -append "ro root=fsRoot rootfstype=9p rootflags=trans=virtio,cache=mmap console=ttyS0 hugepages=${HUGEPAGES} init=${INIT}" + } + +run_in_vm diff --git a/test/test_vm_tap.py b/test/test_vm_tap.py new file mode 100644 index 00000000000..6787ca1e3d7 --- /dev/null +++ b/test/test_vm_tap.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import unittest +from ipaddress import ip_interface +from vpp_qemu_utils import create_namespace +from vpp_iperf import VppIperf +from framework import VppTestCase, VppTestRunner +from config import config + + +class TestTapQemu(VppTestCase): + """Test Tap interfaces inside a QEMU VM. + + Start an iPerf connection stream between QEMU and VPP via + tap v2 interfaces. + + Linux_ns1 -- iperf_client -- tap1 -- VPP-BD -- tap2 -- + -- iperfServer -- Linux_ns2 + """ + + @classmethod + def setUpClass(cls): + super(TestTapQemu, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + super(TestTapQemu, cls).tearDownClass() + + def setUp(self): + """Perform test setup before running QEMU tests. + + 1. Create a namespace for the iPerf Server & Client. + 2. Create 2 tap interfaces in VPP & add them to each namespace. + 3. Add the tap interfaces to a bridge-domain. + """ + super(TestTapQemu, self).setUp() + self.client_namespace = "iprf_client_ns" + self.server_namespace = "iprf_server_ns" + self.client_ip4_prefix = "10.0.0.101/24" + self.server_ip4_prefix = "10.0.0.102/24" + create_namespace(self.client_namespace) + create_namespace(self.server_namespace) + tap1_if_idx = self.create_tap( + 101, self.client_namespace, self.client_ip4_prefix + ) + tap2_if_idx = self.create_tap( + 102, self.server_namespace, self.server_ip4_prefix + ) + self.l2_connect_interfaces(tap1_if_idx, tap2_if_idx) + + def create_tap(self, id, host_namespace, host_ip4_prefix): + result = self.vapi.api( + self.vapi.papi.tap_create_v2, + { + "id": id, + "use_random_mac": True, + "host_namespace_set": True, + "host_namespace": host_namespace, + "host_if_name_set": False, + "host_bridge_set": False, + "host_mac_addr_set": False, + "host_ip4_prefix": ip_interface(host_ip4_prefix), + "host_ip4_prefix_set": True, + }, + ) + sw_if_index = result.sw_if_index + self.vapi.api( + self.vapi.papi.sw_interface_set_flags, + {"sw_if_index": sw_if_index, "flags": 1}, + ) + return sw_if_index + + def dump_vpp_tap_interfaces(self): + return self.vapi.api(self.vapi.papi.sw_interface_tap_v2_dump, {}) + + def dump_bridge_domain_details(self): + return self.vapi.api(self.vapi.papi.bridge_domain_dump, {"bd_id": 1}) + + def l2_connect_interfaces(self, *sw_if_idxs): + for if_idx in sw_if_idxs: + self.vapi.api( + self.vapi.papi.sw_interface_set_l2_bridge, + { + "rx_sw_if_index": if_idx, + "bd_id": 1, + "shg": 0, + "port_type": 0, + "enable": True, + }, + ) + + @unittest.skipUnless(config.extended, "part of extended tests") + def test_tap_iperf(self): + """Start an iperf connection stream between QEMU & VPP via tap.""" + iperf = VppIperf() + iperf.client_ns = self.client_namespace + iperf.server_ns = self.server_namespace + iperf.server_ip = str(ip_interface(self.server_ip4_prefix).ip) + iperf.start() + + +if __name__ == "__main__": + unittest.main(testRunner=VppTestRunner) diff --git a/test/vpp_iperf.py b/test/vpp_iperf.py new file mode 100644 index 00000000000..78ce9d08c90 --- /dev/null +++ b/test/vpp_iperf.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +# Start an iPerf connection stream between two Linux namespaces ## + +import subprocess +import os + + +class VppIperf: + """ "Create an iPerf connection stream between two namespaces. + + Usage: + iperf = VppIperf() # Create the iPerf Object + iperf.client_ns = 'ns1' # Client Namespace + iperf.server_ns = 'ns2' # Server Namespace + iperf.server_ip = '10.0.0.102' # Server IP Address + iperf.start() # Start the connection stream + + Optional: + iperf.duration = 15 # Time to transmit for in seconds (Default=10) + + ## Optionally set any iperf client & server args + Example: + # Run 4 parallel streams, write to logfile & bind to port 5202 + iperf.client_args='-P 4 --logfile /tmp/vpp-vm-tests/vpp_iperf.log -p 5202' + iperf.server_args='-p 5202' + """ + + def __init__(self, server_ns=None, client_ns=None, server_ip=None): + self.server_ns = server_ns + self.client_ns = client_ns + self.server_ip = server_ip + self.duration = 10 + self.client_args = "" + self.server_args = "" + # Set the iperf executable + self.iperf = os.path.join(os.getenv("TEST_DATA_DIR") or "/", "usr/bin/iperf") + + def ensure_init(self): + if self.server_ns and self.client_ns and self.server_ip: + return True + else: + raise Exception( + "Error: Cannot Start." "iPerf object has not been initialized" + ) + + def start_iperf_server(self): + print("Starting iPerf Server Daemon in Namespace ", self.server_ns) + args = [ + "ip", + "netns", + "exec", + self.server_ns, + self.iperf, + "-s", + "-D", + "-B", + self.server_ip, + ] + args.extend(self.server_args.split()) + try: + subprocess.run( + args, + stderr=subprocess.STDOUT, + timeout=self.duration + 5, + encoding="utf-8", + ) + except subprocess.TimeoutExpired as e: + raise Exception("Error: Timeout expired for iPerf", e.output) + + def start_iperf_client(self): + print("Starting iPerf Client in Namespace ", self.client_ns) + args = [ + "ip", + "netns", + "exec", + self.client_ns, + self.iperf, + "-c", + self.server_ip, + "-t", + str(self.duration), + ] + args.extend(self.client_args.split()) + try: + subprocess.run( + args, + stderr=subprocess.STDOUT, + timeout=self.duration + 5, + encoding="utf-8", + ) + except subprocess.TimeoutExpired as e: + raise Exception("Error: Timeout expired for iPerf", e.output) + + def start(self): + """Run iPerf and return True if successful""" + self.ensure_init() + try: + self.start_iperf_server() + except Exception as e: + subprocess.run(["pkill", "iperf"]) + raise Exception("Error starting iPerf Server", e) + + try: + self.start_iperf_client() + except Exception as e: + raise Exception("Error starting iPerf Client", e) + subprocess.run(["pkill", "iperf"]) + + +if __name__ == "__main__": + # Run iPerf using default settings + iperf = VppIperf() + iperf.client_ns = "ns1" + iperf.server_ns = "ns2" + iperf.server_ip = "10.0.0.102" + iperf.duration = 20 + iperf.start() diff --git a/test/vpp_qemu_utils.py b/test/vpp_qemu_utils.py new file mode 100644 index 00000000000..50fc1c865a6 --- /dev/null +++ b/test/vpp_qemu_utils.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +# Utility functions for QEMU tests ## + +import subprocess + + +def create_namespace(ns): + try: + subprocess.run(["ip", "netns", "add", ns]) + except subprocess.CalledProcessError as e: + raise Exception("Error creating namespace:", e.output) + + +def list_namespace(ns): + """List the IP address of a namespace""" + try: + subprocess.run(["ip", "netns", "exec", ns, "ip", "addr"]) + except subprocess.CalledProcessError as e: + raise Exception("Error listing namespace IP:", e.output) -- 2.16.6