From 74ae8ce2710a988ceebbfdf21a646734a70edf89 Mon Sep 17 00:00:00 2001 From: Dave Tucker Date: Thu, 13 Jan 2022 14:55:10 +0000 Subject: [PATCH] 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 --- .github/workflows/build-aya.yml | 42 ++++ test/.gitignore | 3 + test/README.md | 42 ++++ test/cases/000_smoke/000_xdp/pass.ebpf.rs | 30 +++ test/cases/000_smoke/000_xdp/pass.rs | 19 ++ test/cases/000_smoke/000_xdp/test.sh | 29 +++ test/cases/000_smoke/group.sh | 36 +++ .../cases/010_load/000_name/name_test.ebpf.rs | 30 +++ test/cases/010_load/000_name/name_test.rs | 20 ++ test/cases/010_load/000_name/test.sh | 32 +++ test/cases/010_load/group.sh | 36 +++ test/cases/_lib/lib.sh | 229 ++++++++++++++++++ test/cases/group.sh | 36 +++ 13 files changed, 584 insertions(+) create mode 100644 test/.gitignore create mode 100644 test/README.md create mode 100644 test/cases/000_smoke/000_xdp/pass.ebpf.rs create mode 100755 test/cases/000_smoke/000_xdp/pass.rs create mode 100755 test/cases/000_smoke/000_xdp/test.sh create mode 100755 test/cases/000_smoke/group.sh create mode 100644 test/cases/010_load/000_name/name_test.ebpf.rs create mode 100755 test/cases/010_load/000_name/name_test.rs create mode 100755 test/cases/010_load/000_name/test.sh create mode 100755 test/cases/010_load/group.sh create mode 100644 test/cases/_lib/lib.sh create mode 100755 test/cases/group.sh diff --git a/.github/workflows/build-aya.yml b/.github/workflows/build-aya.yml index 9b5756a3..ab7546cd 100644 --- a/.github/workflows/build-aya.yml +++ b/.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 \ No newline at end of file diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..19caa99c --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,3 @@ +_results +_tmp +_images \ No newline at end of file diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..3cef36c7 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test/cases/000_smoke/000_xdp/pass.ebpf.rs b/test/cases/000_smoke/000_xdp/pass.ebpf.rs new file mode 100644 index 00000000..1097209e --- /dev/null +++ b/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 { + Ok(xdp_action::XDP_PASS) +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} \ No newline at end of file diff --git a/test/cases/000_smoke/000_xdp/pass.rs b/test/cases/000_smoke/000_xdp/pass.rs new file mode 100755 index 00000000..f5ca55c8 --- /dev/null +++ b/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..."); +} diff --git a/test/cases/000_smoke/000_xdp/test.sh b/test/cases/000_smoke/000_xdp/test.sh new file mode 100755 index 00000000..974e033c --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test/cases/000_smoke/group.sh b/test/cases/000_smoke/group.sh new file mode 100755 index 00000000..d60a423d --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test/cases/010_load/000_name/name_test.ebpf.rs b/test/cases/010_load/000_name/name_test.ebpf.rs new file mode 100644 index 00000000..e3cb6d21 --- /dev/null +++ b/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 { + Ok(xdp_action::XDP_PASS) +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} \ No newline at end of file diff --git a/test/cases/010_load/000_name/name_test.rs b/test/cases/010_load/000_name/name_test.rs new file mode 100755 index 00000000..0e9c9bd4 --- /dev/null +++ b/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)); +} diff --git a/test/cases/010_load/000_name/test.sh b/test/cases/010_load/000_name/test.sh new file mode 100755 index 00000000..1fbabb9f --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test/cases/010_load/group.sh b/test/cases/010_load/group.sh new file mode 100755 index 00000000..74bab3a7 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/test/cases/_lib/lib.sh b/test/cases/_lib/lib.sh new file mode 100644 index 00000000..f6a7e7e1 --- /dev/null +++ b/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" < "${AYA_TMPDIR}/user-data.yaml" < "${AYA_TMPDIR}/ssh_config" <