tests: run a test inside a QEMU VM 37/32637/23
authorNaveen Joy <najoy@cisco.com>
Tue, 11 May 2021 17:31:18 +0000 (10:31 -0700)
committerNaveen Joy <najoy@cisco.com>
Thu, 4 Aug 2022 19:19:35 +0000 (12:19 -0700)
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 <najoy@cisco.com>
test/Makefile
test/run.py [new file with mode: 0755]
test/scripts/run_vpp_in_vm.sh [new file with mode: 0755]
test/test_vm_tap.py [new file with mode: 0644]
test/vpp_iperf.py [new file with mode: 0644]
test/vpp_qemu_utils.py [new file with mode: 0644]

index 08c6eab..e5c2b2d 100644 (file)
@@ -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 (executable)
index 0000000..07b24d5
--- /dev/null
@@ -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 (executable)
index 0000000..8b8d14e
--- /dev/null
@@ -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 (file)
index 0000000..6787ca1
--- /dev/null
@@ -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 (file)
index 0000000..78ce9d0
--- /dev/null
@@ -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 (file)
index 0000000..50fc1c8
--- /dev/null
@@ -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)