diff --git a/.github/workflows/build-aya.yml b/.github/workflows/build-aya.yml index 90a3b6ff..9d523875 100644 --- a/.github/workflows/build-aya.yml +++ b/.github/workflows/build-aya.yml @@ -38,14 +38,35 @@ jobs: test: runs-on: ubuntu-20.04 needs: build - container: - image: ghcr.io/aya-rs/aya-test-rtf:main steps: - uses: actions/checkout@v2 + - uses: actions/checkout@v2 + with: + repository: libbpf/libbpf + path: libbpf + + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: rustfmt, clippy, rust-src + target: x86_64-unknown-linux-musl + override: true + + - uses: Swatinem/rust-cache@v1 + + - name: Install Pre-requisites + run: | + sudo apt-get -qy install linux-tools-common qemu-system-x86 cloud-image-utils openssh-client libelf-dev gcc-multilib + cargo install bpf-linker + + + - name: Lint integration tests + run: | + cargo xtask build-integration-test-ebpf --libbpf-dir ./libbpf + cargo clippy -p integration-test -- --deny warnings + cargo clippy -p integration-test-macros -- --deny warnings - - name: Run regression tests + - name: Run integration tests run: | - ln -s /root/.rustup ${HOME}/.rustup - cd test - rtf -vvv run + (cd test && ./run.sh ../libbpf) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml deleted file mode 100644 index d13d3aae..00000000 --- a/.github/workflows/images.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: aya-test-image - -on: - schedule: - - cron: "42 2 * * 0" - push: - branches: - - 'main' - paths: - - 'images/**' - - '.github/workflows/images.yml' - pull_request: - branches: - - 'main' - paths: - - 'images/**' - - '.github/workflows/images.yml' - -env: - REGISTRY: ghcr.io - IMAGE_NAME: aya-rs/aya-test-rtf - -jobs: - rtf: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v2 - - - name: Log in to the Container registry - uses: docker/login-action@v1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v3 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@v2 - with: - context: images - file: images/Dockerfile.rtf - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 59b3d16f..ffb10c76 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,24 +24,22 @@ jobs: with: profile: minimal toolchain: nightly - components: rustfmt, clippy, miri + components: rustfmt, clippy, miri, rust-src override: true - name: Check formatting run: | cargo fmt --all -- --check - pushd bpf - cargo fmt --all -- --check - popd + (cd bpf && cargo fmt --all -- --check) + (cd test/integration-ebpf && cargo fmt --all -- --check) - name: Run clippy run: | cargo clippy -p aya -- --deny warnings cargo clippy -p aya-gen -- --deny warnings cargo clippy -p xtask -- --deny warnings - pushd bpf - cargo clippy -p aya-bpf -- --deny warnings - popd + (cd bpf && cargo clippy -p aya-bpf -- --deny warnings) + (cd test/integration-ebpf && cargo clippy -- --deny warnings) - name: Run miri env: diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json index 60fe4ee3..4b796ccb 100644 --- a/.vim/coc-settings.json +++ b/.vim/coc-settings.json @@ -1,3 +1,4 @@ { - "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml"] + "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml", "test/integration-ebpf/Cargo.toml"], + "rust-analyzer.checkOnSave.allTargets": false } diff --git a/.vscode/settings.json b/.vscode/settings.json index 60fe4ee3..4b796ccb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml"] + "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml", "test/integration-ebpf/Cargo.toml"], + "rust-analyzer.checkOnSave.allTargets": false } diff --git a/Cargo.toml b/Cargo.toml index 0eed0f81..c5a9a2e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] -members = ["aya", "aya-gen", "xtask"] +members = ["aya", "aya-gen", "test/integration-test", "test/integration-test-macros", "xtask"] +default-members = ["aya", "aya-gen"] diff --git a/bpf/rustfmt.toml b/bpf/rustfmt.toml new file mode 120000 index 00000000..39f97b04 --- /dev/null +++ b/bpf/rustfmt.toml @@ -0,0 +1 @@ +../rustfmt.toml \ No newline at end of file diff --git a/images/Dockerfile.rtf b/images/Dockerfile.rtf deleted file mode 100644 index 72f28cf1..00000000 --- a/images/Dockerfile.rtf +++ /dev/null @@ -1,38 +0,0 @@ -FROM fedora:35 - -# Rust Nightly -RUN curl https://sh.rustup.rs -sSf | sh -s -- \ - --default-toolchain nightly \ - --component rustfmt \ - --component clippy \ - --component rust-src \ - --target x86_64-unknown-linux-musl \ - -y - -ENV PATH "/root/.cargo/bin:$PATH" - -# Pre-requisites -RUN dnf install \ - --setopt=install_weak_deps=False --best -qy \ - golang \ - qemu-system-x86 \ - cloud-utils \ - genisoimage \ - libbpf-devel \ - clang \ - openssl-devel \ - musl-libc \ - git && dnf clean all \ - && rm -rf /var/cache/yum - -RUN cargo install \ - bpf-linker \ - rust-script \ - sccache - -RUN go install github.com/linuxkit/rtf@latest -ENV PATH "/root/go/bin:$PATH" -ENV RUSTC_WRAPPER "sccache" - -ENTRYPOINT ["rtf"] -CMD ["-vvv", "run"] diff --git a/test/.gitignore b/test/.gitignore index 19caa99c..d36977dc 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,3 +1 @@ -_results -_tmp -_images \ No newline at end of file +.tmp diff --git a/test/README.md b/test/README.md index 164f5b56..81430af2 100644 --- a/test/README.md +++ b/test/README.md @@ -1,48 +1,52 @@ -Aya Regression Tests -==================== +Aya Integration Tests +===================== -The aya regression test suite is a set of tests to ensure that +The aya integration 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 +### Linux -1. `rustup target 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` +To run locally all you need is: -It is not required, but the tests run significantly faster if you use `sccache` +1. Rust nightly +1. A checkout of `libbpf` +1. `cargo install bpf-linker` +1. `bpftool` -You may also use the docker image to run the tests: +### Other OSs -``` -docker run -it --rm --device /dev/kvm -v/home/dave/dev/aya-rs/aya:/src -w /src/test ghcr.io/aya-rs/aya-test-rtf:main -``` +1. A POSIX shell +1. A checkout of `libbpf` +1. `rustup target add x86_64-unknown-linux-musl` +1. `cargo install bpf-linker` +1. Install `qemu` and `cloud-init-utils` package - or any package that provides `cloud-localds` ## Usage -To read more about how to use `rtf`, see the [documentation](https://github.com/linuxkit/rtf/blob/master/docs/USER_GUIDE.md) +From the root of this repository: -### Run the tests with verbose output +### Native ``` -rtf -vvv run +cargo xtask integration-test --libbpf-dir /path/to/libbpf ``` -### Run the tests using an older kernel + +### Virtualized + ``` -AYA_TEST_IMAGE=centos8 rtf -vvv run +./test/run.sh /path/to/libbpf ``` - ### 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 +Tests should follow these guidelines: -See `./cases` for examples \ No newline at end of file +- Rust eBPF code should live in `integration-ebpf/${NAME}.rs` and included in `integration-ebpf/Cargo.toml` +- C eBPF code should live in `integration-test/src/bpf/${NAME}.bpf.c`. It's automatically compiled and made available as `${OUT_DIR}/${NAME}.bpf.o`. +- Any bytecode should be included in the integration test binary using `include_bytes_aligned!` +- Tests should be added to `integration-test/src/test` +- You may add a new module, or use an existing one +- Integration tests must use the `#[integration_test]` macro to be included in the build +- Test functions should return `anyhow::Result<()>` since this allows the use of `?` to return errors. +- You may either `panic!` when an assertion fails or `bail!`. The former is preferred since the stack trace will point directly to the failed line. diff --git a/test/cases/000_smoke/000_xdp/pass.rs b/test/cases/000_smoke/000_xdp/pass.rs deleted file mode 100755 index f5ca55c8..00000000 --- a/test/cases/000_smoke/000_xdp/pass.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! ```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 deleted file mode 100755 index eba23581..00000000 --- a/test/cases/000_smoke/000_xdp/test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/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 ${NAME}.o ${NAME} - exec_vm rm -f ${NAME} ${NAME}.o -} - -trap clean_up EXIT - -# Test code goes here -compile_ebpf "$(pwd)/${NAME}.ebpf.rs" -compile_user "$(pwd)/${NAME}.rs" - -scp_vm ${NAME}.o -scp_vm ${NAME} - -exec_vm sudo ./${NAME} - -exit 0 \ No newline at end of file diff --git a/test/cases/000_smoke/010_ext/ext.rs b/test/cases/000_smoke/010_ext/ext.rs deleted file mode 100755 index 909a848b..00000000 --- a/test/cases/000_smoke/010_ext/ext.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! ```cargo -//! [dependencies] -//! aya = { path = "../../../../aya" } -//! ``` - -use aya::{ - Bpf, BpfLoader, - programs::{Extension, ProgramFd, Xdp, XdpFlags}, -}; -use std::convert::TryInto; - -fn main() { - println!("Loading Root XDP program"); - let mut bpf = Bpf::load_file("main.o").unwrap(); - let pass: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); - pass.load().unwrap(); - pass.attach("lo", XdpFlags::default()).unwrap(); - - println!("Loading Extension Program"); - let mut bpf = BpfLoader::new().extension("drop").load_file("ext.o").unwrap(); - let drop_: &mut Extension = bpf.program_mut("drop").unwrap().try_into().unwrap(); - drop_.load(pass.fd().unwrap(), "xdp_pass").unwrap(); - println!("Success..."); -} diff --git a/test/cases/000_smoke/010_ext/test.sh b/test/cases/000_smoke/010_ext/test.sh deleted file mode 100755 index 695849a5..00000000 --- a/test/cases/000_smoke/010_ext/test.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/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=ext - -clean_up() { - rm -rf main.o ${NAME}.o ${NAME} - exec_vm rm -f main.o ${NAME}.o ${NAME} -} - -trap clean_up EXIT - -# Test code goes here -min_kernel_version 5.9 - -compile_c_ebpf "$(pwd)/main.bpf.c" -compile_c_ebpf "$(pwd)/${NAME}.bpf.c" -compile_user "$(pwd)/${NAME}.rs" - -scp_vm main.o -scp_vm ${NAME}.o -scp_vm ${NAME} - -exec_vm sudo ./${NAME} - -exit 0 \ No newline at end of file diff --git a/test/cases/000_smoke/group.sh b/test/cases/000_smoke/group.sh deleted file mode 100755 index d60a423d..00000000 --- a/test/cases/000_smoke/group.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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.rs b/test/cases/010_load/000_name/name_test.rs deleted file mode 100755 index 0e9c9bd4..00000000 --- a/test/cases/010_load/000_name/name_test.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! ```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 deleted file mode 100755 index 1fbabb9f..00000000 --- a/test/cases/010_load/000_name/test.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/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/010_multiple_maps/multimap.rs b/test/cases/010_load/010_multiple_maps/multimap.rs deleted file mode 100755 index 48d7548c..00000000 --- a/test/cases/010_load/010_multiple_maps/multimap.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! ```cargo -//! [dependencies] -//! log = "0.4" -//! simplelog = "0.11" -//! aya = { path = "../../../../aya" } -//! ``` - -use aya::{ - Bpf, - programs::{Xdp, XdpFlags}, -}; -use log::info; -use std::convert::TryInto; - -use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; - -fn main() { - TermLogger::init( - LevelFilter::Debug, - ConfigBuilder::new() - .set_target_level(LevelFilter::Error) - .set_location_level(LevelFilter::Error) - .build(), - TerminalMode::Mixed, - ColorChoice::Auto, - ).unwrap(); - info!("Loading XDP program"); - let mut bpf = Bpf::load_file("multimap.o").unwrap(); - let pass: &mut Xdp = bpf.program_mut("stats").unwrap().try_into().unwrap(); - pass.load().unwrap(); - pass.attach("eth0", XdpFlags::default()).unwrap(); - info!("Success..."); -} diff --git a/test/cases/010_load/010_multiple_maps/test.sh b/test/cases/010_load/010_multiple_maps/test.sh deleted file mode 100755 index 75def301..00000000 --- a/test/cases/010_load/010_multiple_maps/test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh -# SUMMARY: Check that a program with multiple maps in the maps section loads -# LABELS: - -set -e - -# Source libraries. Uncomment if needed/defined -#. "${RT_LIB}" -. "${RT_PROJECT_ROOT}/_lib/lib.sh" - -NAME=multimap - -clean_up() { - rm -rf ${NAME}.o ${NAME} - exec_vm rm -f ${NAME}.o ${NAME} -} - -trap clean_up EXIT - -# Test code goes here -compile_c_ebpf "$(pwd)/${NAME}.bpf.c" -compile_user "$(pwd)/${NAME}.rs" - -scp_vm ${NAME}.o -scp_vm ${NAME} - -exec_vm sudo ./${NAME} - -exit 0 \ No newline at end of file diff --git a/test/cases/010_load/030_unload/test.rs b/test/cases/010_load/030_unload/test.rs deleted file mode 100755 index ed0d725b..00000000 --- a/test/cases/010_load/030_unload/test.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! ```cargo -//! [dependencies] -//! aya = { path = "../../../../aya" } -//! ``` - -use aya::{ - programs::{Xdp, XdpFlags}, - Bpf, -}; -use std::convert::TryInto; -use std::process::Command; - -fn is_loaded() -> bool { - let output = Command::new("bpftool").args(&["prog"]).output().unwrap(); - let stdout = String::from_utf8(output.stdout).unwrap(); - stdout.contains("test_unload") -} - -fn assert_loaded(loaded: bool) { - let state = is_loaded(); - if state == loaded { - return; - } - panic!("Expected loaded: {} but was loaded: {}", loaded, state); -} - -fn main() { - println!("Loading XDP program"); - let mut bpf = Bpf::load_file("test.o").unwrap(); - let dispatcher: &mut Xdp = bpf.program_mut("test_unload").unwrap().try_into().unwrap(); - - dispatcher.load().unwrap(); - - let link = dispatcher.attach("eth0", XdpFlags::default()).unwrap(); - - { - let link_owned = dispatcher.take_link(link); - - dispatcher.unload().unwrap(); - - assert_loaded(true); - }; - - assert_loaded(false); - - dispatcher.load().unwrap(); - - assert_loaded(true); - - dispatcher.attach("eth0", XdpFlags::default()).unwrap(); - - assert_loaded(true); - - dispatcher.unload().unwrap(); - - assert_loaded(false); -} diff --git a/test/cases/010_load/030_unload/test.sh b/test/cases/010_load/030_unload/test.sh deleted file mode 100755 index efe20416..00000000 --- a/test/cases/010_load/030_unload/test.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -# SUMMARY: Check that the program can be unloaded -# LABELS: - -set -ex - -# Source libraries. Uncomment if needed/defined -#. "${RT_LIB}" -. "${RT_PROJECT_ROOT}/_lib/lib.sh" - -NAME=test - -clean_up() { - rm -rf ebpf user ${NAME}.o ${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} \ No newline at end of file diff --git a/test/cases/010_load/group.sh b/test/cases/010_load/group.sh deleted file mode 100755 index 74bab3a7..00000000 --- a/test/cases/010_load/group.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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/020_elf/000_maps/map_test.o b/test/cases/020_elf/000_maps/map_test.o deleted file mode 100644 index 522a86d2..00000000 Binary files a/test/cases/020_elf/000_maps/map_test.o and /dev/null differ diff --git a/test/cases/020_elf/000_maps/test.sh b/test/cases/020_elf/000_maps/test.sh deleted file mode 100755 index 3ce0f4cc..00000000 --- a/test/cases/020_elf/000_maps/test.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -# SUMMARY: Check that maps are correctly represented in ELF files -# LABELS: - -set -ex - -# Source libraries. Uncomment if needed/defined -#. "${RT_LIB}" -. "${RT_PROJECT_ROOT}/_lib/lib.sh" - -NAME=map_test - -clean_up() { - rm -rf ebpf user ${NAME}.o -} - -trap clean_up EXIT - -# Test code goes here -compile_ebpf ${NAME}.ebpf.rs - -readelf --sections ${NAME}.o | grep -q "maps" -readelf --syms ${NAME}.o | grep -q "BAR" - -exit 0 \ No newline at end of file diff --git a/test/cases/020_elf/group.sh b/test/cases/020_elf/group.sh deleted file mode 100644 index 9a46609c..00000000 --- a/test/cases/020_elf/group.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -# SUMMARY: Tests to check ELF from aya-bpf -# 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/compile-ebpf.ers b/test/cases/_lib/compile-ebpf.ers deleted file mode 100644 index 66993d20..00000000 --- a/test/cases/_lib/compile-ebpf.ers +++ /dev/null @@ -1,85 +0,0 @@ -//! ```cargo -//! [dependencies] -//! libbpf-sys = { version = "0.6.1-1" } -//! anyhow = "1" -//! ``` - -use std::{ - env, - fs::{self, OpenOptions}, - io::Write, - path::Path, - process::Command, - string::String, -}; -use anyhow::{bail, Context, Result}; -static CLANG_DEFAULT: &str = "/usr/bin/clang"; - -/// Extract vendored libbpf headers from libbpf-sys. -fn extract_libbpf_headers>(include_path: P) -> Result<()> { - let dir = include_path.as_ref().join("bpf"); - fs::create_dir_all(&dir)?; - for (filename, contents) in libbpf_sys::API_HEADERS.iter() { - let path = dir.as_path().join(filename); - let mut file = OpenOptions::new().write(true).create(true).open(path)?; - file.write_all(contents.as_bytes())?; - } - - Ok(()) -} - -/// Build eBPF programs with clang and libbpf headers. -fn build_ebpf>(in_file: P, out_file: P, include_path: P) -> Result<()> { - extract_libbpf_headers(include_path.clone())?; - let clang = match env::var("CLANG") { - Ok(val) => val, - Err(_) => String::from(CLANG_DEFAULT), - }; - let arch = match std::env::consts::ARCH { - "x86_64" => "x86", - "aarch64" => "arm64", - _ => std::env::consts::ARCH, - }; - let mut cmd = Command::new(clang); - cmd.arg(format!("-I{}", include_path.as_ref().to_string_lossy())) - .arg("-g") - .arg("-O2") - .arg("-target") - .arg("bpf") - .arg("-c") - .arg(format!("-D__TARGET_ARCH_{}", arch)) - .arg(in_file.as_ref().as_os_str()) - .arg("-o") - .arg(out_file.as_ref().as_os_str()); - - let output = cmd.output().context("Failed to execute clang")?; - if !output.status.success() { - bail!( - "Failed to compile eBPF programs\n \ - stdout=\n \ - {}\n \ - stderr=\n \ - {}\n", - String::from_utf8(output.stdout).unwrap(), - String::from_utf8(output.stderr).unwrap() - ); - } - - Ok(()) -} - -fn main() -> Result<()> { - let args: Vec = env::args().collect(); - if args.len() != 3 { - bail!("requires 2 arguments. src and dst") - } - let path = env::current_dir()?; - let src = Path::new(&args[1]); - let dst = Path::new(&args[2]); - - let include_path = path.join("include"); - fs::create_dir_all(include_path.clone())?; - build_ebpf(src, dst, &include_path)?; - - Ok(()) -} \ No newline at end of file diff --git a/test/cases/group.sh b/test/cases/group.sh deleted file mode 100755 index b1362948..00000000 --- a/test/cases/group.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/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 diff --git a/test/integration-ebpf/.cargo/config.toml b/test/integration-ebpf/.cargo/config.toml new file mode 100644 index 00000000..d89a98b0 --- /dev/null +++ b/test/integration-ebpf/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "../../target" +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml new file mode 100644 index 00000000..b2a526fa --- /dev/null +++ b/test/integration-ebpf/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "integration-ebpf" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +aya-bpf = { path = "../../bpf/aya-bpf" } + +[[bin]] +name = "map_test" +path = "src/map_test.rs" + +[[bin]] +name = "name_test" +path = "src/name_test.rs" + +[[bin]] +name = "pass" +path = "src/pass.rs" + +[[bin]] +name = "test" +path = "src/test.rs" + +[profile.dev] +panic = "abort" +opt-level = 2 +overflow-checks = false + +[profile.release] +panic = "abort" +debug = 2 + +[workspace] +members = [] diff --git a/test/integration-ebpf/rust-toolchain.toml b/test/integration-ebpf/rust-toolchain.toml new file mode 100644 index 00000000..c046a094 --- /dev/null +++ b/test/integration-ebpf/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel="nightly" diff --git a/test/integration-ebpf/rustfmt.toml b/test/integration-ebpf/rustfmt.toml new file mode 120000 index 00000000..760eb840 --- /dev/null +++ b/test/integration-ebpf/rustfmt.toml @@ -0,0 +1 @@ +../../rustfmt.toml \ No newline at end of file diff --git a/test/cases/000_smoke/010_ext/ext.bpf.c b/test/integration-ebpf/src/bpf/ext.bpf.c similarity index 65% rename from test/cases/000_smoke/010_ext/ext.bpf.c rename to test/integration-ebpf/src/bpf/ext.bpf.c index f0da9445..6c77127a 100644 --- a/test/cases/000_smoke/010_ext/ext.bpf.c +++ b/test/integration-ebpf/src/bpf/ext.bpf.c @@ -1,6 +1,5 @@ #include #include -#include SEC("xdp/drop") int xdp_drop(struct xdp_md *ctx) @@ -8,4 +7,4 @@ int xdp_drop(struct xdp_md *ctx) return XDP_DROP; } -char _license[] SEC("license") = "GPL"; \ No newline at end of file +char _license[] SEC("license") = "GPL"; diff --git a/test/cases/000_smoke/010_ext/main.bpf.c b/test/integration-ebpf/src/bpf/main.bpf.c similarity index 65% rename from test/cases/000_smoke/010_ext/main.bpf.c rename to test/integration-ebpf/src/bpf/main.bpf.c index fbe4600b..79c82041 100644 --- a/test/cases/000_smoke/010_ext/main.bpf.c +++ b/test/integration-ebpf/src/bpf/main.bpf.c @@ -1,6 +1,5 @@ #include #include -#include SEC("xdp/pass") int xdp_pass(struct xdp_md *ctx) @@ -8,4 +7,4 @@ int xdp_pass(struct xdp_md *ctx) return XDP_PASS; } -char _license[] SEC("license") = "GPL"; \ No newline at end of file +char _license[] SEC("license") = "GPL"; diff --git a/test/cases/010_load/010_multiple_maps/multimap.bpf.c b/test/integration-ebpf/src/bpf/multimap.bpf.c similarity index 96% rename from test/cases/010_load/010_multiple_maps/multimap.bpf.c rename to test/integration-ebpf/src/bpf/multimap.bpf.c index 82ad2832..615935eb 100644 --- a/test/cases/010_load/010_multiple_maps/multimap.bpf.c +++ b/test/integration-ebpf/src/bpf/multimap.bpf.c @@ -44,4 +44,4 @@ int xdp_stats(struct xdp_md *ctx) return XDP_PASS; } -char _license[] SEC("license") = "GPL"; \ No newline at end of file +char _license[] SEC("license") = "GPL"; diff --git a/test/cases/020_elf/000_maps/map_test.ebpf.rs b/test/integration-ebpf/src/map_test.rs similarity index 87% rename from test/cases/020_elf/000_maps/map_test.ebpf.rs rename to test/integration-ebpf/src/map_test.rs index c8d45df3..fc17d1f9 100644 --- a/test/cases/020_elf/000_maps/map_test.ebpf.rs +++ b/test/integration-ebpf/src/map_test.rs @@ -1,16 +1,11 @@ -//! ```cargo -//! [dependencies] -//! aya-bpf = { path = "../../../../bpf/aya-bpf" } -//! ``` - #![no_std] #![no_main] use aya_bpf::{ bindings::xdp_action, macros::{map, xdp}, - programs::XdpContext, maps::Array, + programs::XdpContext, }; #[map] diff --git a/test/cases/010_load/000_name/name_test.ebpf.rs b/test/integration-ebpf/src/name_test.rs similarity index 57% rename from test/cases/010_load/000_name/name_test.ebpf.rs rename to test/integration-ebpf/src/name_test.rs index e3cb6d21..f4f1e315 100644 --- a/test/cases/010_load/000_name/name_test.ebpf.rs +++ b/test/integration-ebpf/src/name_test.rs @@ -1,19 +1,10 @@ -//! ```cargo -//! [dependencies] -//! aya-bpf = { path = "../../../../bpf/aya-bpf" } -//! ``` - #![no_std] #![no_main] -use aya_bpf::{ - bindings::xdp_action, - macros::xdp, - programs::XdpContext, -}; +use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext}; -#[xdp(name="ihaveaverylongname")] -pub fn pass(ctx: XdpContext) -> u32 { +#[xdp(name = "ihaveaverylongname")] +pub fn ihaveaverylongname(ctx: XdpContext) -> u32 { match unsafe { try_pass(ctx) } { Ok(ret) => ret, Err(_) => xdp_action::XDP_ABORTED, @@ -27,4 +18,4 @@ unsafe fn try_pass(_ctx: XdpContext) -> Result { #[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.ebpf.rs b/test/integration-ebpf/src/pass.rs similarity index 65% rename from test/cases/000_smoke/000_xdp/pass.ebpf.rs rename to test/integration-ebpf/src/pass.rs index 1097209e..0979d557 100644 --- a/test/cases/000_smoke/000_xdp/pass.ebpf.rs +++ b/test/integration-ebpf/src/pass.rs @@ -1,18 +1,9 @@ -//! ```cargo -//! [dependencies] -//! aya-bpf = { path = "../../../../bpf/aya-bpf" } -//! ``` - #![no_std] #![no_main] -use aya_bpf::{ - bindings::xdp_action, - macros::xdp, - programs::XdpContext, -}; +use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext}; -#[xdp(name="pass")] +#[xdp(name = "pass")] pub fn pass(ctx: XdpContext) -> u32 { match unsafe { try_pass(ctx) } { Ok(ret) => ret, @@ -27,4 +18,4 @@ unsafe fn try_pass(_ctx: XdpContext) -> Result { #[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/030_unload/test.ebpf.rs b/test/integration-ebpf/src/test.rs similarity index 84% rename from test/cases/010_load/030_unload/test.ebpf.rs rename to test/integration-ebpf/src/test.rs index a6ed58dd..d0ff7b32 100644 --- a/test/cases/010_load/030_unload/test.ebpf.rs +++ b/test/integration-ebpf/src/test.rs @@ -1,8 +1,3 @@ -//! ```cargo -//! [dependencies] -//! aya-bpf = { path = "../../../../bpf/aya-bpf" } -//! ``` - #![no_std] #![no_main] diff --git a/test/integration-test-macros/Cargo.toml b/test/integration-test-macros/Cargo.toml new file mode 100644 index 00000000..57d928e1 --- /dev/null +++ b/test/integration-test-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "integration-test-macros" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +quote = "1" +syn = {version = "1.0", features = ["full"]} + +[lib] +proc-macro = true diff --git a/test/integration-test-macros/src/lib.rs b/test/integration-test-macros/src/lib.rs new file mode 100644 index 00000000..6f69b486 --- /dev/null +++ b/test/integration-test-macros/src/lib.rs @@ -0,0 +1,19 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +#[proc_macro_attribute] +pub fn integration_test(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item = parse_macro_input!(item as ItemFn); + let name = &item.sig.ident; + let name_str = &item.sig.ident.to_string(); + let expanded = quote! { + #item + + inventory::submit!(IntegrationTest { + name: concat!(module_path!(), "::", #name_str), + test_fn: #name, + }); + }; + TokenStream::from(expanded) +} diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml new file mode 100644 index 00000000..decae299 --- /dev/null +++ b/test/integration-test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "integration-test" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +anyhow = "1" +aya = { path = "../../aya" } +inventory = "0.2" +integration-test-macros = { path = "../integration-test-macros" } +lazy_static = "1" +libc = { version = "0.2.105" } +log = "0.4" +object = { version = "0.29", default-features = false, features = ["std", "read_core", "elf"] } +regex = "1" +simplelog = "0.12" diff --git a/test/integration-test/src/main.rs b/test/integration-test/src/main.rs new file mode 100644 index 00000000..f3f61b3a --- /dev/null +++ b/test/integration-test/src/main.rs @@ -0,0 +1,27 @@ +use log::info; +use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; + +mod tests; +use tests::IntegrationTest; + +fn main() -> anyhow::Result<()> { + TermLogger::init( + LevelFilter::Debug, + ConfigBuilder::new() + .set_target_level(LevelFilter::Error) + .set_location_level(LevelFilter::Error) + .build(), + TerminalMode::Mixed, + ColorChoice::Auto, + )?; + + // Run the tests + for t in inventory::iter:: { + info!("Running {}", t.name); + if let Err(e) = (t.test_fn)() { + panic!("{}", e) + } + } + + Ok(()) +} diff --git a/test/integration-test/src/tests/elf.rs b/test/integration-test/src/tests/elf.rs new file mode 100644 index 00000000..f139dfaf --- /dev/null +++ b/test/integration-test/src/tests/elf.rs @@ -0,0 +1,27 @@ +use super::{integration_test, IntegrationTest}; + +use anyhow::bail; +use aya::include_bytes_aligned; +use object::{Object, ObjectSymbol}; + +#[integration_test] +fn test_maps() -> anyhow::Result<()> { + let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/map_test"); + let obj_file = object::File::parse(bytes)?; + if obj_file.section_by_name("maps").is_none() { + bail!("No 'maps' ELF section"); + } + let mut found = false; + for sym in obj_file.symbols() { + if let Ok(name) = sym.name() { + if name == "BAR" { + found = true; + break; + } + } + } + if !found { + bail!("No symbol 'BAR' in ELF file") + } + Ok(()) +} diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs new file mode 100644 index 00000000..bacbf317 --- /dev/null +++ b/test/integration-test/src/tests/load.rs @@ -0,0 +1,75 @@ +use std::{convert::TryInto, process::Command}; + +use aya::{ + include_bytes_aligned, + programs::{Xdp, XdpFlags}, + Bpf, +}; + +use super::{integration_test, IntegrationTest}; + +#[integration_test] +fn long_name() -> anyhow::Result<()> { + let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/name_test"); + let mut bpf = Bpf::load(bytes)?; + let name_prog: &mut Xdp = bpf.program_mut("ihaveaverylongname").unwrap().try_into()?; + name_prog.load().unwrap(); + name_prog.attach("lo", XdpFlags::default())?; + + // We used to be able to assert with bpftool that the program name was short. + // It seem though that it now uses the name from the ELF symbol table instead. + // Therefore, as long as we were able to load the program, this is good enough. + + Ok(()) +} + +#[integration_test] +fn multiple_maps() -> anyhow::Result<()> { + let bytes = + include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/multimap.bpf.o"); + let mut bpf = Bpf::load(bytes)?; + let pass: &mut Xdp = bpf.program_mut("stats").unwrap().try_into().unwrap(); + pass.load().unwrap(); + pass.attach("lo", XdpFlags::default()).unwrap(); + Ok(()) +} + +fn is_loaded() -> bool { + let output = Command::new("bpftool").args(&["prog"]).output().unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + stdout.contains("test_unload") +} + +fn assert_loaded(loaded: bool) { + let state = is_loaded(); + if state == loaded { + return; + } + panic!("Expected loaded: {} but was loaded: {}", loaded, state); +} + +#[integration_test] +fn unload() -> anyhow::Result<()> { + let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/test"); + let mut bpf = Bpf::load(bytes)?; + let prog: &mut Xdp = bpf.program_mut("test_unload").unwrap().try_into().unwrap(); + prog.load().unwrap(); + let link = prog.attach("lo", XdpFlags::default()).unwrap(); + { + let _link_owned = prog.take_link(link); + prog.unload().unwrap(); + assert_loaded(true); + }; + + assert_loaded(false); + prog.load().unwrap(); + + assert_loaded(true); + prog.attach("lo", XdpFlags::default()).unwrap(); + + assert_loaded(true); + prog.unload().unwrap(); + + assert_loaded(false); + Ok(()) +} diff --git a/test/integration-test/src/tests/mod.rs b/test/integration-test/src/tests/mod.rs new file mode 100644 index 00000000..2dd5524a --- /dev/null +++ b/test/integration-test/src/tests/mod.rs @@ -0,0 +1,37 @@ +use anyhow::bail; +use lazy_static::lazy_static; +use libc::{uname, utsname}; +use regex::Regex; +use std::{ffi::CStr, mem}; + +pub mod elf; +pub mod load; +pub mod smoke; + +pub use integration_test_macros::integration_test; +#[derive(Debug)] +pub struct IntegrationTest { + pub name: &'static str, + pub test_fn: fn() -> anyhow::Result<()>, +} + +pub(crate) fn kernel_version() -> anyhow::Result<(u8, u8, u8)> { + lazy_static! { + static ref RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)").unwrap(); + } + let mut data: utsname = unsafe { mem::zeroed() }; + let ret = unsafe { uname(&mut data) }; + assert!(ret >= 0, "libc::uname failed."); + let release_cstr = unsafe { CStr::from_ptr(data.release.as_ptr()) }; + let release = release_cstr.to_string_lossy(); + if let Some(caps) = RE.captures(&release) { + let major = caps.get(1).unwrap().as_str().parse().unwrap(); + let minor = caps.get(2).unwrap().as_str().parse().unwrap(); + let patch = caps.get(3).unwrap().as_str().parse().unwrap(); + Ok((major, minor, patch)) + } else { + bail!("no kernel version found"); + } +} + +inventory::collect!(IntegrationTest); diff --git a/test/integration-test/src/tests/smoke.rs b/test/integration-test/src/tests/smoke.rs new file mode 100644 index 00000000..a4474764 --- /dev/null +++ b/test/integration-test/src/tests/smoke.rs @@ -0,0 +1,45 @@ +use std::convert::TryInto; + +use aya::{ + include_bytes_aligned, + programs::{Extension, Xdp, XdpFlags}, + Bpf, BpfLoader, +}; +use log::info; + +use super::{integration_test, kernel_version, IntegrationTest}; + +#[integration_test] +fn xdp() -> anyhow::Result<()> { + let bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/pass"); + let mut bpf = Bpf::load(bytes)?; + let dispatcher: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); + dispatcher.load().unwrap(); + dispatcher.attach("lo", XdpFlags::default()).unwrap(); + Ok(()) +} + +#[integration_test] +fn extension() -> anyhow::Result<()> { + let (major, minor, _) = kernel_version()?; + if major < 5 || minor < 9 { + info!( + "skipping as {}.{} does not meet version requirement of 5.9", + major, minor + ); + return Ok(()); + } + // TODO: Check kernel version == 5.9 or later + let main_bytes = + include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/main.bpf.o"); + let mut bpf = Bpf::load(main_bytes)?; + let pass: &mut Xdp = bpf.program_mut("pass").unwrap().try_into().unwrap(); + pass.load().unwrap(); + pass.attach("lo", XdpFlags::default()).unwrap(); + + let ext_bytes = include_bytes_aligned!("../../../../target/bpfel-unknown-none/debug/ext.bpf.o"); + let mut bpf = BpfLoader::new().extension("drop").load(ext_bytes).unwrap(); + let drop_: &mut Extension = bpf.program_mut("drop").unwrap().try_into().unwrap(); + drop_.load(pass.fd().unwrap(), "xdp_pass").unwrap(); + Ok(()) +} diff --git a/test/cases/_lib/lib.sh b/test/run.sh old mode 100644 new mode 100755 similarity index 63% rename from test/cases/_lib/lib.sh rename to test/run.sh index 2df42b8f..f144842b --- a/test/cases/_lib/lib.sh +++ b/test/run.sh @@ -1,16 +1,12 @@ #!/bin/sh -# Source the main regression test library if present -[ -f "${RT_LIB}" ] && . "${RT_LIB}" +set -e # Temporary directory for tests to use. -AYA_TMPDIR="${RT_PROJECT_ROOT}/_tmp" +AYA_TMPDIR="$(pwd)/.tmp" # Directory for VM images -AYA_IMGDIR="${RT_PROJECT_ROOT}/_images" - -# Cancel Exit Code -RT_CANCEL=253 +AYA_IMGDIR=${AYA_TMPDIR} # Test Architecture if [ -z "${AYA_TEST_ARCH}" ]; then @@ -27,71 +23,6 @@ case "${AYA_TEST_IMAGE}" in 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" - rm -rf "${dir}/ebpf" -} - -# compile a C BPF file -compile_c_ebpf() { - file=$(basename "$1") - dir=$(dirname "$1") - base=$(echo "${file}" | cut -f1 -d '.') - - rust-script "${RT_PROJECT_ROOT}/_lib/compile-ebpf.ers" "${1}" "${dir}/${base}.o" - rm -rf "${dir}/include" -} - -# 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}" - rm -rf "${dir}/user" -} - download_images() { mkdir -p "${AYA_IMGDIR}" case $1 in @@ -214,10 +145,11 @@ EOF scp_vm() { local=$1 + remote=$(basename "$1") scp -q -F "${AYA_TMPDIR}/ssh_config" \ -i "${AYA_TMPDIR}/test_rsa" \ -P 2222 "${local}" \ - "${AYA_SSH_USER}@localhost:${local}" + "${AYA_SSH_USER}@localhost:${remote}" } exec_vm() { @@ -243,19 +175,14 @@ cleanup_vm() { fi } -# Check that host machine meets minimum kernel requirement -# Must be in format {major}.{minor} -min_kernel_version() { - target_major=$(echo "$1" | cut -d '.' -f1) - target_minor=$(echo "$1" | cut -d '.' -f2) +if [ -z "$1" ]; then + echo "path to libbpf required" + exit 1 +fi - vm_kernel=$(exec_vm uname -r) - vm_major=$(echo "${vm_kernel}" | cut -d '.' -f1) - vm_minor=$(echo "${vm_kernel}" | cut -d '.' -f2) +start_vm +trap stop_vm EXIT - if [ "${vm_major}" -lt "${target_major}" ] || [ "${vm_minor}" -lt "${target_minor}" ]; then - echo "Test not supported on kernel ${vm_major}.${vm_minor}" - return ${RT_CANCEL} - fi - return 0 -} \ No newline at end of file +cargo xtask build-integration-test --musl --libbpf-dir "$1" +scp_vm ../target/x86_64-unknown-linux-musl/debug/integration-test +exec_vm sudo ./integration-test diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 6a709059..9f8a3820 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -13,3 +13,5 @@ quote = "1" proc-macro2 = "1" indexmap = "1.6" indoc = "1.0" +lazy_static = "1" +serde_json = "1" diff --git a/xtask/src/build_ebpf.rs b/xtask/src/build_ebpf.rs new file mode 100644 index 00000000..1ba16b2d --- /dev/null +++ b/xtask/src/build_ebpf.rs @@ -0,0 +1,162 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{bail, Context}; +use clap::Parser; + +use crate::utils::WORKSPACE_ROOT; + +#[derive(Debug, Copy, Clone)] +pub enum Architecture { + BpfEl, + BpfEb, +} + +impl std::str::FromStr for Architecture { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "bpfel-unknown-none" => Architecture::BpfEl, + "bpfeb-unknown-none" => Architecture::BpfEb, + _ => return Err("invalid target".to_owned()), + }) + } +} + +impl std::fmt::Display for Architecture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Architecture::BpfEl => "bpfel-unknown-none", + Architecture::BpfEb => "bpfeb-unknown-none", + }) + } +} + +#[derive(Debug, Parser)] +pub struct Options { + /// Set the endianness of the BPF target + #[clap(default_value = "bpfel-unknown-none", long)] + pub target: Architecture, + /// Build the release target + #[clap(long)] + pub release: bool, + /// Libbpf dir, required for compiling C code + #[clap(long, action)] + pub libbpf_dir: PathBuf, +} + +pub fn build_ebpf(opts: Options) -> anyhow::Result<()> { + build_rust_ebpf(&opts)?; + build_c_ebpf(&opts) +} + +fn build_rust_ebpf(opts: &Options) -> anyhow::Result<()> { + let mut dir = PathBuf::from(WORKSPACE_ROOT.to_string()); + dir.push("test/integration-ebpf"); + + let target = format!("--target={}", opts.target); + let mut args = vec![ + "+nightly", + "build", + "--verbose", + target.as_str(), + "-Z", + "build-std=core", + ]; + if opts.release { + args.push("--release") + } + let status = Command::new("cargo") + .current_dir(&dir) + .args(&args) + .status() + .expect("failed to build bpf program"); + assert!(status.success()); + Ok(()) +} + +fn get_libbpf_headers>(libbpf_dir: P, include_path: P) -> anyhow::Result<()> { + let dir = include_path.as_ref(); + fs::create_dir_all(&dir)?; + let status = Command::new("make") + .current_dir(libbpf_dir.as_ref().join("src")) + .arg(format!("INCLUDEDIR={}", dir.as_os_str().to_string_lossy())) + .arg("install_headers") + .status() + .expect("failed to build get libbpf headers"); + assert!(status.success()); + Ok(()) +} + +fn build_c_ebpf(opts: &Options) -> anyhow::Result<()> { + let mut src = PathBuf::from(WORKSPACE_ROOT.to_string()); + src.push("test/integration-ebpf/src/bpf"); + + let mut out_path = PathBuf::from(WORKSPACE_ROOT.to_string()); + out_path.push("target"); + out_path.push(opts.target.to_string()); + out_path.push(if opts.release { "release " } else { "debug" }); + + let include_path = out_path.join("include"); + get_libbpf_headers(&opts.libbpf_dir, &include_path)?; + let files = fs::read_dir(&src).unwrap(); + for file in files { + let p = file.unwrap().path(); + if let Some(ext) = p.extension() { + if ext == "c" { + let mut out = PathBuf::from(&out_path); + out.push(p.file_name().unwrap()); + out.set_extension("o"); + compile_with_clang(&p, &out, &include_path)?; + } + } + } + Ok(()) +} + +/// Build eBPF programs with clang and libbpf headers. +fn compile_with_clang>( + src: P, + out: P, + include_path: P, +) -> anyhow::Result<()> { + let clang = match env::var("CLANG") { + Ok(val) => val, + Err(_) => String::from("/usr/bin/clang"), + }; + let arch = match std::env::consts::ARCH { + "x86_64" => "x86", + "aarch64" => "arm64", + _ => std::env::consts::ARCH, + }; + let mut cmd = Command::new(clang); + cmd.arg(format!("-I{}", include_path.as_ref().to_string_lossy())) + .arg("-g") + .arg("-O2") + .arg("-target") + .arg("bpf") + .arg("-c") + .arg(format!("-D__TARGET_ARCH_{}", arch)) + .arg(src.as_ref().as_os_str()) + .arg("-o") + .arg(out.as_ref().as_os_str()); + + let output = cmd.output().context("Failed to execute clang")?; + if !output.status.success() { + bail!( + "Failed to compile eBPF programs\n \ + stdout=\n \ + {}\n \ + stderr=\n \ + {}\n", + String::from_utf8(output.stdout).unwrap(), + String::from_utf8(output.stderr).unwrap() + ); + } + + Ok(()) +} diff --git a/xtask/src/build_test.rs b/xtask/src/build_test.rs new file mode 100644 index 00000000..f2cf91b1 --- /dev/null +++ b/xtask/src/build_test.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use std::process::Command; + +use crate::build_ebpf; + +#[derive(Parser)] +pub struct Options { + /// Whether to compile for the musl libc target + #[clap(short, long)] + pub musl: bool, + + #[clap(flatten)] + pub ebpf_options: build_ebpf::Options, +} + +pub fn build_test(opts: Options) -> anyhow::Result<()> { + build_ebpf::build_ebpf(opts.ebpf_options)?; + + let mut args = vec!["build", "-p", "integration-test", "--verbose"]; + if opts.musl { + args.push("--target=x86_64-unknown-linux-musl"); + } + let status = Command::new("cargo") + .args(&args) + .status() + .expect("failed to build bpf program"); + assert!(status.success()); + Ok(()) +} diff --git a/xtask/src/docs/mod.rs b/xtask/src/docs/mod.rs index e8d1fb73..0496ef63 100644 --- a/xtask/src/docs/mod.rs +++ b/xtask/src/docs/mod.rs @@ -30,13 +30,7 @@ pub fn docs() -> Result<(), anyhow::Error> { header.flush().expect("couldn't flush contents"); let abs_header_path = fs::canonicalize(&header_path).unwrap(); - let args = vec![ - "+nightly", - "doc", - "--workspace", - "--no-deps", - "--all-features", - ]; + let args = vec!["+nightly", "doc", "--no-deps", "--all-features"]; let status = Command::new("cargo") .current_dir(&working_dir) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 406efd9f..5cf9574c 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,5 +1,9 @@ +mod build_ebpf; +mod build_test; mod codegen; mod docs; +mod run; +pub(crate) mod utils; use std::process::exit; @@ -14,6 +18,9 @@ pub struct Options { enum Command { Codegen(codegen::Options), Docs, + BuildIntegrationTest(build_test::Options), + BuildIntegrationTestEbpf(build_ebpf::Options), + IntegrationTest(run::Options), } fn main() { @@ -23,6 +30,9 @@ fn main() { let ret = match opts.command { Codegen(opts) => codegen::codegen(opts), Docs => docs::docs(), + BuildIntegrationTest(opts) => build_test::build_test(opts), + BuildIntegrationTestEbpf(opts) => build_ebpf::build_ebpf(opts), + IntegrationTest(opts) => run::run(opts), }; if let Err(e) = ret { diff --git a/xtask/src/run.rs b/xtask/src/run.rs new file mode 100644 index 00000000..59e24431 --- /dev/null +++ b/xtask/src/run.rs @@ -0,0 +1,72 @@ +use std::{os::unix::process::CommandExt, path::PathBuf, process::Command}; + +use anyhow::Context as _; +use clap::Parser; + +use crate::build_ebpf::{build_ebpf, Architecture, Options as BuildOptions}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Set the endianness of the BPF target + #[clap(default_value = "bpfel-unknown-none", long)] + pub bpf_target: Architecture, + /// Build and run the release target + #[clap(long)] + pub release: bool, + /// The command used to wrap your application + #[clap(short, long, default_value = "sudo -E")] + pub runner: String, + /// libbpf directory + #[clap(long, action)] + pub libbpf_dir: String, + /// Arguments to pass to your application + #[clap(name = "args", last = true)] + pub run_args: Vec, +} + +/// Build the project +fn build(opts: &Options) -> Result<(), anyhow::Error> { + let mut args = vec!["build"]; + if opts.release { + args.push("--release") + } + args.push("--workspace"); + let status = Command::new("cargo") + .args(&args) + .status() + .expect("failed to build userspace"); + assert!(status.success()); + Ok(()) +} + +/// Build and run the project +pub fn run(opts: Options) -> Result<(), anyhow::Error> { + // build our ebpf program followed by our application + build_ebpf(BuildOptions { + target: opts.bpf_target, + release: opts.release, + libbpf_dir: PathBuf::from(&opts.libbpf_dir), + }) + .context("Error while building eBPF program")?; + build(&opts).context("Error while building userspace application")?; + + // profile we are building (release or debug) + let profile = if opts.release { "release" } else { "debug" }; + let bin_path = format!("target/{}/integration-test", profile); + + // arguments to pass to the application + let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect(); + + // configure args + let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect(); + args.push(bin_path.as_str()); + args.append(&mut run_args); + + // spawn the command + let err = Command::new(args.first().expect("No first argument")) + .args(args.iter().skip(1)) + .exec(); + + // we shouldn't get here unless the command failed to spawn + Err(anyhow::Error::from(err).context(format!("Failed to run `{}`", args.join(" ")))) +} diff --git a/xtask/src/utils.rs b/xtask/src/utils.rs new file mode 100644 index 00000000..fcfb918c --- /dev/null +++ b/xtask/src/utils.rs @@ -0,0 +1,17 @@ +use lazy_static::lazy_static; +use serde_json::Value; +use std::process::Command; + +lazy_static! { + pub static ref WORKSPACE_ROOT: String = workspace_root(); +} + +fn workspace_root() -> String { + let output = Command::new("cargo").arg("metadata").output().unwrap(); + if !output.status.success() { + panic!("unable to run cargo metadata") + } + let stdout = String::from_utf8(output.stdout).unwrap(); + let v: Value = serde_json::from_str(&stdout).unwrap(); + v["workspace_root"].as_str().unwrap().to_string() +}