diff --git a/.gitignore b/.gitignore index 0248fbf2..bf144acc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ libbpf/ site/ header.html .idea/ + +kerneltest/initramfs.cpio +kerneltest/kernels/* diff --git a/kerneltest/kernels/.gitkeep b/kerneltest/kernels/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 671a987c..9e737c12 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -14,3 +14,10 @@ proc-macro2 = "1" indoc = "2.0" lazy_static = "1" serde_json = "1" +reqwest = { version = "0.11.18", default-features = false, features = [ + "blocking", + "rustls-tls", +] } +which = "4.4.0" +itertools = "0.11.0" +tempfile = "3.6.0" diff --git a/xtask/src/integration_test.rs b/xtask/src/integration_test.rs index b4f5dd15..eda8fdfe 100644 --- a/xtask/src/integration_test.rs +++ b/xtask/src/integration_test.rs @@ -27,51 +27,64 @@ pub struct Options { /// Configures building the integration test binary. pub struct BuildOptions { pub release: bool, + + /// The target triple to build for. + pub target: Option, } /// Build the project. Returns the path to the binary that was built. -pub fn build(opts: BuildOptions) -> Result { - let BuildOptions { release } = opts; +pub fn build(opts: BuildOptions) -> Result { let mut args = vec!["build"]; if opts.release { args.push("--release") } args.push("-p"); args.push("integration-test"); + let target_path = if let Some(target) = &opts.target { + args.push("--target"); + args.push(target); + format!("{target}/") + } else { + String::new() + }; let status = Command::new("cargo") .args(&args) .status() .expect("failed to build userspace"); assert!(status.success()); - let profile = if release { "release" } else { "debug" }; - let bin_path = format!("target/{profile}/integration-test"); - Ok(PathBuf::from(bin_path)) + let profile = if opts.release { "release" } else { "debug" }; + Ok(format!("target/{target_path}{profile}/integration-test")) } /// Build and run the project pub fn run(opts: Options) -> Result<(), anyhow::Error> { let Options { - bpf_target, - release, runner, - libbpf_dir, run_args, + bpf_target, + libbpf_dir, + release, } = opts; + // build our ebpf program followed by our application build_ebpf(BuildEbpfOptions { target: bpf_target, libbpf_dir, }) .context("Error while building eBPF program")?; - let bin_path = - build(BuildOptions { release }).context("Error while building userspace application")?; + + let bin_path = build(BuildOptions { + release, + target: None, + }) + .context("Error while building userspace application")?; // arguments to pass to the application let mut run_args: Vec<_> = run_args.iter().map(String::as_str).collect(); // configure args let mut args: Vec<_> = runner.trim().split_terminator(' ').collect(); - args.push(bin_path.to_str().expect("Invalid binary path")); + args.push(&bin_path); args.append(&mut run_args); // spawn the command diff --git a/xtask/src/kernel_test.rs b/xtask/src/kernel_test.rs new file mode 100644 index 00000000..a58e5d26 --- /dev/null +++ b/xtask/src/kernel_test.rs @@ -0,0 +1,149 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::Context as _; +use clap::Parser; +use itertools::Itertools; + +use crate::{build_ebpf, integration_test}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Set the endianness of the BPF target + #[clap(default_value = "bpfel-unknown-none", long)] + pub bpf_target: build_ebpf::Architecture, + /// Build and run the release target + #[clap(long)] + pub release: bool, + /// libbpf directory + #[clap(long, action)] + pub libbpf_dir: PathBuf, + /// Kernel version to use. + #[clap(name = "kernel-version", long)] + pub kernel_version: String, + /// Path to the kerneltest root. + #[clap(name = "kerneltest-root", long, default_value = "kerneltest")] + pub kerneltest_root: String, + /// Arguments to pass to your application + #[clap(name = "args", last = true)] + pub run_args: Vec, +} + +/// MUSL_TARGET is the target triple for the musl libc +/// platform. It defaults to statically linking binaries. +/// We need a statically linked binary to run in the initramfs +/// created by bluebox. +const MUSL_TARGET: &str = "x86_64-unknown-linux-musl"; + +pub(crate) fn kernel_test(opts: Options) -> Result<(), anyhow::Error> { + let Options { + bpf_target, + release, + libbpf_dir, + run_args, + kernel_version, + kerneltest_root, + } = opts; + build_ebpf::build_ebpf(build_ebpf::BuildEbpfOptions { + target: bpf_target, + libbpf_dir, + }) + .context("failed to build ebpf")?; + let integration_test_bin = integration_test::build(integration_test::BuildOptions { + release, + target: Some(String::from(MUSL_TARGET)), + })?; + let initramfs_path = build_initramfs(&kerneltest_root, &integration_test_bin, &run_args)?; + let kernel_image_path = get_kernel(&kernel_version, &kerneltest_root)?; + run(&initramfs_path, &kernel_image_path) +} + +const BLUEBOX_BINARY: &str = "bluebox"; + +fn build_initramfs( + kerneltest_root: &str, + integration_test_bin: &str, + integration_test_args: &[String], +) -> Result { + let args = match integration_test_args { + [] => String::new(), + _ => std::iter::once(":\"") + .chain(itertools::Itertools::intersperse( + integration_test_args.iter().map(|v| v.as_str()), + " ", + )) + .chain(std::iter::once("\"")) + .collect_vec() + .join(""), + }; + let initramfs_path = [kerneltest_root, "initramfs.cpio"].join(std::path::MAIN_SEPARATOR_STR); + which::which(BLUEBOX_BINARY) + .with_context(|| format!("{BLUEBOX_BINARY} not found")) + .context("try installing with `go install github.com/florianl/bluebox@latest`")?; + let args = format!("{integration_test_bin}{args}"); + match Command::new(BLUEBOX_BINARY) + .arg("-e") + .arg(&args) + .arg("-o") + .arg(&initramfs_path) + .status() + { + Err(err) => Err(anyhow::anyhow!("failed to build initramfs: {}", err)), + Ok(status) if !status.success() => Err(anyhow::anyhow!( + "failed to build initramfs: status code {}", + status + )), + Ok(_) => Ok(initramfs_path), + } +} + +fn get_kernel(kernel_version: &str, kerneltest_root: &str) -> Result { + let kernel_name = format!("linux-{}.bz", kernel_version); + let image_path_str = + [kerneltest_root, "kernels", &kernel_name].join(std::path::MAIN_SEPARATOR_STR); + let image_path = Path::new(&image_path_str); + if image_path.exists() && image_path.is_file() { + return Ok(image_path_str); + } + + let mut tmp = tempfile::NamedTempFile::new().context("creating temp file for kernel image")?; + let url = format!( + "https://github.com/cilium/ci-kernels/raw/a15c0b2aa7cf32640c03764fa79b0a815608ddce/linux-{kernel_version}.bz" + ); + + reqwest::blocking::get(&url) + .and_then(|v| v.error_for_status()) + .with_context(|| format!("fetching kernel at {}", &url))? + .copy_to(&mut tmp) + .context("writing kernel file")?; + tmp.persist_noclobber(image_path) + .context(format!("persisting kernel file {:?}", &image_path))?; + Ok(image_path_str) +} + +const QEMU_BINARY: &str = "qemu-system-x86_64"; + +fn run(initramfs: &str, kernel_image: &str) -> Result<(), anyhow::Error> { + which::which(QEMU_BINARY).with_context(|| format!("{QEMU_BINARY} not found"))?; + let args = vec![ + "-no-reboot", + "-append", + "printk.devkmsg=on kernel.panic=-1 crashkernel=256M", + "-kernel", + kernel_image, + "-initrd", + initramfs, + "-nographic", + "-append", + "console=ttyS0", + "-m", + "1.5G", + ]; + match Command::new(QEMU_BINARY).args(args).status() { + Err(err) => Err(anyhow::anyhow!("failed to run qemu: {}", err)), + Ok(status) if !status.success() => Err(anyhow::anyhow!("failed to run qemu: {}", status)), + Ok(_) => Ok(()), + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index e50e8675..72db7f45 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,6 +3,7 @@ mod build_test; mod codegen; mod docs; mod integration_test; +mod kernel_test; pub(crate) mod utils; @@ -22,6 +23,7 @@ enum Command { BuildIntegrationTest(build_test::Options), BuildIntegrationTestEbpf(build_ebpf::BuildEbpfOptions), IntegrationTest(integration_test::Options), + KernelTest(kernel_test::Options), } fn main() { @@ -34,6 +36,7 @@ fn main() { BuildIntegrationTest(opts) => build_test::build_test(opts), BuildIntegrationTestEbpf(opts) => build_ebpf::build_ebpf(opts), IntegrationTest(opts) => integration_test::run(opts), + KernelTest(opts) => kernel_test::kernel_test(opts), }; if let Err(e) = ret {