Parcourir la source

test: Add regression tests

This uses a mix of rust-script, bash, qemu and a test runner called RTF
to add a regression test suite... and wires it into GitHub Actions

Signed-off-by: Dave Tucker <[email protected]>
Dave Tucker il y a 3 ans
Parent
commit
74ae8ce271

+ 42 - 0
.github/workflows/build-aya.yml

@@ -26,3 +26,45 @@ jobs:
 
       - name: Run tests
         run: RUST_BACKTRACE=full cargo test --verbose
+
+  test:
+    runs-on: ubuntu-20.04
+    needs: build
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: nightly
+          components: rustfmt, clippy, rust-src
+          override: true
+          target: x86_64-unknown-linux-musl
+
+      - uses: Swatinem/rust-cache@v1
+
+      - name: Set up Go 1.17
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.17
+
+      - name: Set GOPATH
+        run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
+        env:
+          GOPATH: ${{runner.workspace}}
+
+      - name: Install prereqs
+        run: |
+          go install github.com/linuxkit/rtf@latest
+          cargo install bpf-linker
+          cargo install rust-script
+          cargo install sccache
+          echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
+          export DEBIAN_FRONTEND=noninteractive
+          sudo apt-get update
+          sudo apt-get install -qy qemu-utils qemu-system-x86 cloud-image-utils genisoimage
+
+      - name: Run regression tests
+        run: |
+          cd test
+          rtf -vvv run

+ 3 - 0
test/.gitignore

@@ -0,0 +1,3 @@
+_results
+_tmp
+_images

+ 42 - 0
test/README.md

@@ -0,0 +1,42 @@
+Aya Regression Tests
+====================
+
+The aya regression test suite is a set of tests to ensure that
+common usage behaviours work on real Linux distros
+## Prerequisites
+
+This assumes you have a working Rust and Go toolchain on the host machine
+
+1. `rustup toolcahin add x86_64-unknown-linux-musl`
+1. Install [`rtf`](https://github.com/linuxkit/rtf): `go install github.com/linuxkit/rtf@latest`
+1. Install rust-script: `cargo install rust-script`
+1. Install `qemu` and `cloud-init-utils` package - or any package that provides `cloud-localds`
+
+It is not required, but the tests run significantly faster if you use `sccache`
+
+## Usage
+
+To read more about how to use `rtf`, see the [documentation](https://github.com/linuxkit/rtf/blob/master/docs/USER_GUIDE.md)
+
+### Run the tests with verbose output
+
+```
+rtf -vvv run
+```
+### Run the tests using an older kernel
+
+```
+AYA_TEST_IMAGE=centos8 rtf -vvv run
+```
+
+### Writing a test
+
+Tests should follow this pattern:
+
+- The eBPF code should be in a file named `${NAME}.ebpf.rs`
+- The userspace code should be in a file named `${NAME}.rs`
+- The userspace program should make assertions and exit with a non-zero return code to signal failure
+- VM start and stop is handled by the framework
+- Any files copied to the VM should be cleaned up afterwards
+
+See `./cases` for examples

+ 30 - 0
test/cases/000_smoke/000_xdp/pass.ebpf.rs

@@ -0,0 +1,30 @@
+//! ```cargo
+//! [dependencies]
+//! aya-bpf = { path = "../../../../bpf/aya-bpf" }
+//! ```
+
+#![no_std]
+#![no_main]
+
+use aya_bpf::{
+    bindings::xdp_action,
+    macros::xdp,
+    programs::XdpContext,
+};
+
+#[xdp(name="pass")]
+pub fn pass(ctx: XdpContext) -> u32 {
+    match unsafe { try_pass(ctx) } {
+        Ok(ret) => ret,
+        Err(_) => xdp_action::XDP_ABORTED,
+    }
+}
+
+unsafe fn try_pass(_ctx: XdpContext) -> Result<u32, u32> {
+    Ok(xdp_action::XDP_PASS)
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    unsafe { core::hint::unreachable_unchecked() }
+}

+ 19 - 0
test/cases/000_smoke/000_xdp/pass.rs

@@ -0,0 +1,19 @@
+//! ```cargo
+//! [dependencies]
+//! aya = { path = "../../../../aya" }
+//! ```
+
+use aya::{
+    Bpf,
+    programs::{Xdp, XdpFlags},
+};
+use std::convert::TryInto;
+
+fn main() {
+    println!("Loading XDP program");
+    let mut bpf = Bpf::load_file("pass.o").unwrap();
+    let dispatcher: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap();
+    dispatcher.load().unwrap();
+    dispatcher.attach("eth0", XdpFlags::default()).unwrap();
+    println!("Success...");
+}

+ 29 - 0
test/cases/000_smoke/000_xdp/test.sh

@@ -0,0 +1,29 @@
+#!/bin/sh
+# SUMMARY: Check that a simple XDP program an be loaded
+# LABELS:
+
+set -e
+
+# Source libraries. Uncomment if needed/defined
+#. "${RT_LIB}"
+. "${RT_PROJECT_ROOT}/_lib/lib.sh"
+
+NAME=pass
+
+clean_up() {
+    rm -rf ebpf user ${NAME}.o ${NAME}
+    exec_vm rm -f pass pass.o
+}
+
+trap clean_up EXIT
+
+# Test code goes here
+compile_ebpf "$(pwd)/${NAME}.ebpf.rs"
+compile_user "$(pwd)/${NAME}.rs"
+
+scp_vm pass.o
+scp_vm pass
+
+exec_vm sudo ./pass
+
+exit 0

+ 36 - 0
test/cases/000_smoke/group.sh

@@ -0,0 +1,36 @@
+#!/bin/sh
+# SUMMARY: Smoke tests to check that simple programs can be loaded on a VM
+# LABELS:
+
+# Source libraries. Uncomment if needed/defined
+# . "${RT_LIB}"
+. "${RT_PROJECT_ROOT}/_lib/lib.sh"
+
+set -e
+
+group_init() {
+    # Group initialisation code goes here
+    return 0
+}
+
+group_deinit() {
+    # Group de-initialisation code goes here
+    return 0
+}
+
+CMD=$1
+case $CMD in
+init)
+    group_init
+    res=$?
+    ;;
+deinit)
+    group_deinit
+    res=$?
+    ;;
+*)
+    res=1
+    ;;
+esac
+
+exit $res

+ 30 - 0
test/cases/010_load/000_name/name_test.ebpf.rs

@@ -0,0 +1,30 @@
+//! ```cargo
+//! [dependencies]
+//! aya-bpf = { path = "../../../../bpf/aya-bpf" }
+//! ```
+
+#![no_std]
+#![no_main]
+
+use aya_bpf::{
+    bindings::xdp_action,
+    macros::xdp,
+    programs::XdpContext,
+};
+
+#[xdp(name="ihaveaverylongname")]
+pub fn pass(ctx: XdpContext) -> u32 {
+    match unsafe { try_pass(ctx) } {
+        Ok(ret) => ret,
+        Err(_) => xdp_action::XDP_ABORTED,
+    }
+}
+
+unsafe fn try_pass(_ctx: XdpContext) -> Result<u32, u32> {
+    Ok(xdp_action::XDP_PASS)
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    unsafe { core::hint::unreachable_unchecked() }
+}

+ 20 - 0
test/cases/010_load/000_name/name_test.rs

@@ -0,0 +1,20 @@
+//! ```cargo
+//! [dependencies]
+//! aya = { path = "../../../../aya" }
+//! ```
+
+use aya::{
+    Bpf,
+    programs::{Xdp, XdpFlags},
+};
+use std::convert::TryInto;
+use std::{thread, time};
+
+fn main() {
+    println!("Loading XDP program");
+    let mut bpf = Bpf::load_file("name_test.o").unwrap();
+    let dispatcher: &mut Xdp = bpf.program_mut("ihaveaverylongname").unwrap().try_into().unwrap();
+    dispatcher.load().unwrap();
+    dispatcher.attach("eth0", XdpFlags::default()).unwrap();
+    thread::sleep(time::Duration::from_secs(20));
+}

+ 32 - 0
test/cases/010_load/000_name/test.sh

@@ -0,0 +1,32 @@
+#!/bin/sh
+# SUMMARY: Check that long names are properly truncated
+# LABELS:
+
+set -e
+
+# Source libraries. Uncomment if needed/defined
+#. "${RT_LIB}"
+. "${RT_PROJECT_ROOT}/_lib/lib.sh"
+
+NAME=name_test
+
+clean_up() {
+    rm -rf ebpf user ${NAME}.o ${NAME}
+    exec_vm sudo pkill -9 ${NAME}
+    exec_vm rm ${NAME} ${NAME}.o
+}
+
+trap clean_up EXIT
+
+# Test code goes here
+compile_ebpf ${NAME}.ebpf.rs
+compile_user ${NAME}.rs
+
+scp_vm ${NAME}.o
+scp_vm ${NAME}
+
+exec_vm sudo ./${NAME}&
+prog_list=$(exec_vm sudo bpftool prog)
+echo "${prog_list}" | grep -q "xdp  name ihaveaverylongn  tag"
+
+exit 0

+ 36 - 0
test/cases/010_load/group.sh

@@ -0,0 +1,36 @@
+#!/bin/sh
+# SUMMARY: Tests to check loader features
+# LABELS:
+
+# Source libraries. Uncomment if needed/defined
+# . "${RT_LIB}"
+. "${RT_PROJECT_ROOT}/_lib/lib.sh"
+
+set -e
+
+group_init() {
+    # Group initialisation code goes here
+    return 0
+}
+
+group_deinit() {
+    # Group de-initialisation code goes here
+    return 0
+}
+
+CMD=$1
+case $CMD in
+init)
+    group_init
+    res=$?
+    ;;
+deinit)
+    group_deinit
+    res=$?
+    ;;
+*)
+    res=1
+    ;;
+esac
+
+exit $res

+ 229 - 0
test/cases/_lib/lib.sh

@@ -0,0 +1,229 @@
+#!/bin/sh
+
+# Source the main regression test library if present
+[ -f "${RT_LIB}" ] && . "${RT_LIB}"
+
+# Temporary directory for tests to use.
+AYA_TMPDIR="${RT_PROJECT_ROOT}/_tmp"
+
+# Directory for VM images
+AYA_IMGDIR="${RT_PROJECT_ROOT}/_images"
+
+# Test Architecture
+if [ -z "${AYA_TEST_ARCH}" ]; then
+    AYA_TEST_ARCH="$(uname -m)"
+fi
+
+# Test Image
+if [ -z "${AYA_TEST_IMAGE}" ]; then
+    AYA_TEST_IMAGE="fedora35"
+fi
+
+case "${AYA_TEST_IMAGE}" in
+    fedora*) AYA_SSH_USER="fedora";;
+    centos*) AYA_SSH_USER="centos";;
+esac
+
+# compiles the ebpf program by using rust-script to create a temporary
+# cargo project in $(pwd)/ebpf. caller must add rm -rf ebpf to the clean_up
+# functionAYA_TEST_ARCH
+compile_ebpf() {
+    file=$(basename "$1")
+    dir=$(dirname "$1")
+    base=$(echo "${file}" | cut -f1 -d '.')
+
+    rm -rf "${dir}/ebpf"
+
+    rust-script --pkg-path "${dir}/ebpf" --gen-pkg-only "$1"
+    artifact=$(sed -n 's/^name = \"\(.*\)\"/\1/p' "${dir}/ebpf/Cargo.toml" | head -n1)
+
+    mkdir -p "${dir}/.cargo"
+    cat > "${dir}/.cargo/config.toml" << EOF
+[build]
+target = "bpfel-unknown-none"
+
+[unstable]
+build-std = ["core"]
+EOF
+    cat >> "${dir}/ebpf/Cargo.toml" << EOF
+[workspace]
+members = []
+EOF
+    # overwrite the rs file as rust-script adds a main fn
+    cp "$1" "${dir}/ebpf/${file}"
+    cargo build -q --manifest-path "${dir}/ebpf/Cargo.toml"
+    mv "${dir}/ebpf/target/bpfel-unknown-none/debug/${artifact}" "${dir}/${base}.o"
+    rm -rf "${dir}/.cargo"
+}
+
+# compiles the userspace program by using rust-script to create a temporary
+# cargo project in $(pwd)/user. caller must add rm -rf ebpf to the clean_up
+# function. this is required since the binary produced has to be run with
+# sudo to load an eBPF program
+compile_user() {
+    file=$(basename "$1")
+    dir=$(dirname "$1")
+    base=$(echo "${file}" | cut -f1 -d '.')
+
+    rm -rf "${dir}/user"
+
+    rust-script --pkg-path "${dir}/user" --gen-pkg-only "$1"
+    artifact=$(sed -n 's/^name = \"\(.*\)\"/\1/p' "${dir}/user/Cargo.toml" | head -n1)
+    cat >> "${dir}/user/Cargo.toml" << EOF
+[workspace]
+members = []
+EOF
+    cargo build -q --release --manifest-path "${dir}/user/Cargo.toml" --target=x86_64-unknown-linux-musl
+    mv "${dir}/user/target/x86_64-unknown-linux-musl/release/${artifact}" "${dir}/${base}"
+}
+
+download_images() {
+    mkdir -p "${AYA_IMGDIR}"
+    case $1 in
+        fedora35)
+            if [ ! -f "${AYA_IMGDIR}/fedora35.${AYA_TEST_ARCH}.qcow2" ]; then
+                IMAGE="Fedora-Cloud-Base-35-1.2.${AYA_TEST_ARCH}.qcow2"
+                IMAGE_URL="https://download.fedoraproject.org/pub/fedora/linux/releases/35/Cloud/${AYA_TEST_ARCH}/images"
+                echo "Downloading: ${IMAGE}, this may take a while..."
+                curl -o "${AYA_IMGDIR}/fedora35.${AYA_TEST_ARCH}.qcow2" -sSL "${IMAGE_URL}/${IMAGE}"
+            fi
+            ;;
+        centos8)
+            if [ ! -f "${AYA_IMGDIR}/centos8.${AYA_TEST_ARCH}.qcow2" ]; then
+                IMAGE="CentOS-8-GenericCloud-8.4.2105-20210603.0.${AYA_TEST_ARCH}.qcow2"
+                IMAGE_URL="https://cloud.centos.org/centos/8/${AYA_TEST_ARCH}/images"
+                echo "Downloading: ${IMAGE}, this may take a while..."
+                curl -o "${AYA_IMGDIR}/centos8.${AYA_TEST_ARCH}.qcow2" -sSL "${IMAGE_URL}/${IMAGE}"
+            fi
+            ;;
+        *)
+            echo "$1 is not a recognized image name"
+            return 1
+            ;;
+    esac
+}
+
+start_vm() {
+    download_images "${AYA_TEST_IMAGE}"
+    # prepare config
+    cat > "${AYA_TMPDIR}/metadata.yaml" <<EOF
+instance-id: iid-local01
+local-hostname: test
+EOF
+
+    if [ ! -f "${AYA_TMPDIR}/test_rsa" ]; then
+        ssh-keygen -t rsa -b 4096 -f "${AYA_TMPDIR}/test_rsa" -N "" -C "" -q
+        pub_key=$(cat "${AYA_TMPDIR}/test_rsa.pub")
+        cat > "${AYA_TMPDIR}/user-data.yaml" <<EOF
+#cloud-config
+ssh_authorized_keys:
+  - ${pub_key}
+EOF
+    fi
+
+    if [ ! -f "${AYA_TMPDIR}/ssh_config" ]; then
+        cat > "${AYA_TMPDIR}/ssh_config" <<EOF
+StrictHostKeyChecking=no
+UserKnownHostsFile=/dev/null
+GlobalKnownHostsFile=/dev/null
+EOF
+    fi
+
+    cloud-localds "${AYA_TMPDIR}/seed.img" "${AYA_TMPDIR}/user-data.yaml" "${AYA_TMPDIR}/metadata.yaml"
+
+    case "${AYA_TEST_ARCH}" in
+        x86_64)
+            QEMU=qemu-system-x86_64
+            machine="q35"
+            cpu="qemu64"
+            if [ "$(uname -m)" = "${AYA_TEST_ARCH}" ]; then
+                if [ -c /dev/kvm ]; then
+                    machine="${machine},accel=kvm"
+                    cpu="host"
+                elif [ "$(uname -s)" = "Darwin" ]; then
+                    machine="${machine},accel=hvf"
+                    cpu="host"
+                fi
+            fi
+            ;;
+        aarch64)
+            QEMU=qemu-system-aarch64
+            machine="virt"
+            cpu="cortex-a57"
+            if [ "$(uname -m)" = "${AYA_TEST_ARCH}" ]; then
+                if [ -c /dev/kvm ]; then
+                    machine="${machine},accel=kvm"
+                    cpu="host"
+                elif [ "$(uname -s)" = "Darwin" ]; then
+                    machine="${machine},accel=hvf"
+                    cpu="host"
+                fi
+            fi
+            ;;
+        *)
+            echo "${AYA_TEST_ARCH} is not supported"
+            return 1
+        ;;
+    esac
+
+    qemu-img create -F qcow2 -f qcow2 -o backing_file="${AYA_IMGDIR}/${AYA_TEST_IMAGE}.${AYA_TEST_ARCH}.qcow2" "${AYA_TMPDIR}/vm.qcow2" || return 1
+    $QEMU \
+        -machine "${machine}" \
+        -cpu "${cpu}" \
+        -m 2G \
+        -display none \
+        -monitor none \
+        -daemonize \
+        -pidfile "${AYA_TMPDIR}/vm.pid" \
+        -device virtio-net-pci,netdev=net0 \
+        -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+        -drive if=virtio,format=qcow2,file="${AYA_TMPDIR}/vm.qcow2" \
+        -drive if=virtio,format=raw,file="${AYA_TMPDIR}/seed.img" || return 1
+
+    trap cleanup_vm EXIT
+    echo "Waiting for SSH on port 2222..."
+    retry=0
+    max_retries=300
+    while ! ssh -q -F "${AYA_TMPDIR}/ssh_config" -o ConnectTimeout=1 -i "${AYA_TMPDIR}/test_rsa" ${AYA_SSH_USER}@localhost -p 2222 echo "Hello VM"; do
+        retry=$((retry+1))
+        if [ ${retry} -gt ${max_retries} ]; then
+            echo "Unable to connect to VM"
+            return 1
+        fi
+        sleep 1
+    done
+
+    echo "VM launched, installing dependencies"
+    exec_vm sudo dnf install -qy bpftool
+}
+
+scp_vm() {
+    local=$1
+    scp -q -F "${AYA_TMPDIR}/ssh_config" \
+        -i "${AYA_TMPDIR}/test_rsa" \
+        -P 2222 "${local}" \
+        "${AYA_SSH_USER}@localhost:${local}"
+}
+
+exec_vm() {
+    ssh -q -F "${AYA_TMPDIR}/ssh_config" \
+        -i "${AYA_TMPDIR}/test_rsa" \
+        -p 2222 \
+        ${AYA_SSH_USER}@localhost \
+        "$@"
+}
+
+stop_vm() {
+    if [ -f "${AYA_TMPDIR}/vm.pid" ]; then
+        echo "Stopping VM forcefully"
+        kill -9 "$(cat "${AYA_TMPDIR}/vm.pid")"
+        rm "${AYA_TMPDIR}/vm.pid"
+    fi
+    rm -f "${AYA_TMPDIR}/vm.qcow2"
+}
+
+cleanup_vm() {
+    if [ "$?" != "0" ]; then
+        stop_vm
+    fi
+}

+ 36 - 0
test/cases/group.sh

@@ -0,0 +1,36 @@
+#!/bin/sh
+# NAME: aya
+# SUMMARY: Aya Regression Tests
+
+# Source libraries. Uncomment if needed/defined
+# . "${RT_LIB}"
+. "${RT_PROJECT_ROOT}/_lib/lib.sh"
+
+group_init() {
+    # Group initialisation code goes here
+    [ -r "${AYA_TMPDIR}" ] && rm -rf "${AYA_TMPDIR}"
+    mkdir "${AYA_TMPDIR}"
+    start_vm
+}
+
+group_deinit() {
+    # Group de-initialisation code goes here
+    stop_vm
+}
+
+CMD=$1
+case $CMD in
+init)
+    group_init
+    res=$?
+    ;;
+deinit)
+    group_deinit
+    res=$?
+    ;;
+*)
+    res=1
+    ;;
+esac
+
+exit $res