Skip to content

Commit 78fe3a8

Browse files
committed
integration-test: Implement running on VMs
This allows integration tests to be run using qemu at an arbitrary kernel version. Note that before this change we were only testing fedora 38 which used 6.2. Now we're testing 6.1 kernels. The tests all pass. Older kernel versions were attempted, but the tests don't all pass. Later work can add more kernel versions to test.
1 parent 3376bbd commit 78fe3a8

File tree

10 files changed

+680
-642
lines changed

10 files changed

+680
-642
lines changed

.github/workflows/ci.yml

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,14 @@ jobs:
139139
--target ${{ matrix.target }} \
140140
-Z build-std=core
141141
142-
build-integration-test:
143-
runs-on: ubuntu-22.04
142+
run-integration-test:
143+
strategy:
144+
fail-fast: false
145+
matrix:
146+
runner:
147+
- macos-12
148+
- ubuntu-22.04
149+
runs-on: ${{ matrix.runner }}
144150
steps:
145151
- uses: actions/checkout@v3
146152
with:
@@ -150,13 +156,12 @@ jobs:
150156
with:
151157
toolchain: nightly
152158
components: rust-src
159+
targets: aarch64-unknown-linux-musl,x86_64-unknown-linux-musl
153160

154161
- uses: Swatinem/rust-cache@v2
155162

156-
- name: bpf-linker
157-
run: cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git
158-
159-
- name: Install dependencies
163+
- name: Install prerequisites
164+
if: runner.os == 'Linux'
160165
# ubuntu-22.04 comes with clang 14[0] which doesn't include support for signed and 64bit
161166
# enum values which was added in clang 15[1].
162167
#
@@ -171,63 +176,78 @@ jobs:
171176
set -euxo pipefail
172177
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
173178
echo deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy main | sudo tee /etc/apt/sources.list.d/llvm.list
174-
sudo apt-get update
175-
sudo apt-get -y install clang gcc-multilib llvm
179+
sudo apt update
180+
sudo apt -y install clang gcc-multilib llvm locate qemu-system-{arm,x86}
176181
177-
- name: Build
182+
- name: bpf-linker
183+
if: runner.os == 'Linux'
184+
run: cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git
185+
186+
- name: Install prerequisites
187+
if: runner.os == 'macOS'
188+
# The clang shipped on macOS doesn't support BPF, so we need LLVM from brew.
189+
#
190+
# We also need LLVM for bpf-linker, see comment below.
178191
run: |
179192
set -euxo pipefail
180-
mkdir -p integration-test-binaries
181-
# See https://doc.rust-lang.org/cargo/reference/profiles.html for the
182-
# names of the builtin profiles. Note that dev builds "debug" targets.
183-
cargo xtask build-integration-test --cargo-arg=--profile=dev | xargs -I % cp % integration-test-binaries/dev
184-
cargo xtask build-integration-test --cargo-arg=--profile=release | xargs -I % cp % integration-test-binaries/release
185-
186-
- uses: actions/upload-artifact@v3
187-
with:
188-
name: integration-test-binaries
189-
path: integration-test-binaries
193+
brew install qemu dpkg pkg-config llvm
194+
echo /usr/local/opt/llvm/bin >> $GITHUB_PATH
190195
191-
run-integration-test:
192-
runs-on: macos-latest
193-
needs: ["build-integration-test"]
194-
steps:
195-
- uses: actions/checkout@v3
196-
with:
197-
sparse-checkout: |
198-
test/run.sh
199-
test/cloud-localds
196+
- name: bpf-linker
197+
if: runner.os == 'macOS'
198+
# NB: rustc doesn't ship libLLVM.so on macOS, so disable proxying (default feature).
199+
run: cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git --no-default-features
200200

201-
- name: Install Pre-requisites
201+
- name: Download debian kernels
202+
if: runner.arch == 'ARM64'
202203
run: |
203-
brew install qemu gnu-getopt coreutils cdrtools
204-
205-
- name: Cache tmp files
206-
uses: actions/cache@v3
207-
with:
208-
path: |
209-
.tmp/*.qcow2
210-
.tmp/test_rsa
211-
.tmp/test_rsa.pub
212-
key: tmp-files-${{ hashFiles('test/run.sh') }}
213-
214-
- uses: actions/download-artifact@v3
215-
with:
216-
name: integration-test-binaries
217-
path: integration-test-binaries
218-
219-
- name: Run integration tests
204+
set -euxo pipefail
205+
mkdir -p test/.tmp/debian-kernels/arm64
206+
# NB: a 4.19 kernel image for arm64 was not available.
207+
# TODO(https://github.com/aya-rs/aya/pull/725): enable tests on kernels before 6.0.
208+
# linux-image-5.10.0-23-cloud-arm64-unsigned_5.10.179-3_arm64.deb \
209+
printf '%s\0' \
210+
linux-image-6.1.0-10-cloud-arm64-unsigned_6.1.38-2_arm64.deb \
211+
linux-image-6.4.0-1-cloud-arm64-unsigned_6.4.4-2_arm64.deb \
212+
| xargs -0 -t -P0 -I {} wget -nd -q -P test/.tmp/debian-kernels/arm64 ftp://ftp.us.debian.org/debian/pool/main/l/linux/{}
213+
214+
- name: Download debian kernels
215+
if: runner.arch == 'X64'
220216
run: |
221217
set -euxo pipefail
222-
find integration-test-binaries -type f -exec chmod +x {} \;
223-
test/run.sh integration-test-binaries
218+
mkdir -p test/.tmp/debian-kernels/amd64
219+
# TODO(https://github.com/aya-rs/aya/pull/725): enable tests on kernels before 6.0.
220+
# linux-image-4.19.0-21-cloud-amd64-unsigned_4.19.249-2_amd64.deb \
221+
# linux-image-5.10.0-23-cloud-amd64-unsigned_5.10.179-3_amd64.deb \
222+
printf '%s\0' \
223+
linux-image-6.1.0-10-cloud-amd64-unsigned_6.1.38-2_amd64.deb \
224+
linux-image-6.4.0-1-cloud-amd64-unsigned_6.4.4-2_amd64.deb \
225+
| xargs -0 -t -P0 -I {} wget -nd -q -P test/.tmp/debian-kernels/amd64 ftp://ftp.us.debian.org/debian/pool/main/l/linux/{}
226+
227+
- name: Alias gtar as tar
228+
if: runner.os == 'macOS'
229+
# macOS tar doesn't support --wildcards which we use below.
230+
run: mkdir tar-is-gtar && ln -s "$(which gtar)" tar-is-gtar/tar && echo "$PWD"/tar-is-gtar >> $GITHUB_PATH
231+
232+
- name: Extract debian kernels
233+
run: |
234+
set -euxo pipefail
235+
find test/.tmp -name '*.deb' -print0 | xargs -t -0 -I {} \
236+
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' --file -"
237+
238+
- name: Run integration tests
239+
run: find test/.tmp -name 'vmlinuz-*' | xargs -t cargo xtask integration-test vm
224240

225241
# Provides a single status check for the entire build workflow.
226242
# This is used for merge automation, like Mergify, since GH actions
227243
# has no concept of "when all status checks pass".
228244
# https://docs.mergify.com/conditions/#validating-all-status-checks
229245
build-workflow-complete:
230-
needs: ["lint", "build-test-aya", "build-test-aya-bpf", "run-integration-test"]
246+
needs:
247+
- lint
248+
- build-test-aya
249+
- build-test-aya-bpf
250+
- run-integration-test
231251
runs-on: ubuntu-latest
232252
steps:
233253
- name: Build Complete

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"aya-log-parser",
77
"aya-obj",
88
"aya-tool",
9+
"init",
910
"test/integration-test",
1011
"xtask",
1112

@@ -29,6 +30,7 @@ default-members = [
2930
"aya-log-parser",
3031
"aya-obj",
3132
"aya-tool",
33+
"init",
3234
# test/integration-test is omitted; including it in this list causes `cargo test` to run its
3335
# tests, and that doesn't work unless they've been built with `cargo xtask`.
3436
"xtask",
@@ -72,6 +74,7 @@ lazy_static = { version = "1", default-features = false }
7274
libc = { version = "0.2.105", default-features = false }
7375
log = { version = "0.4", default-features = false }
7476
netns-rs = { version = "0.1", default-features = false }
77+
nix = { version = "0.26.2", default-features = false }
7578
num_enum = { version = "0.6", default-features = false }
7679
object = { version = "0.31", default-features = false }
7780
parking_lot = { version = "0.12.0", default-features = false }

bpf/aya-log-ebpf/src/lib.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
#![no_std]
22
#![warn(clippy::cast_lossless, clippy::cast_sign_loss)]
33

4-
use aya_bpf::{
5-
macros::map,
6-
maps::{PerCpuArray, PerfEventByteArray},
7-
};
4+
#[cfg(target_arch = "bpf")]
5+
use aya_bpf::macros::map;
6+
use aya_bpf::maps::{PerCpuArray, PerfEventByteArray};
87
pub use aya_log_common::{write_record_header, Level, WriteToBuf, LOG_BUF_CAPACITY};
98
pub use aya_log_ebpf_macros::{debug, error, info, log, trace, warn};
109

@@ -15,11 +14,19 @@ pub struct LogBuf {
1514
}
1615

1716
#[doc(hidden)]
18-
#[map]
17+
// This cfg_attr prevents compilation failures on macOS where the generated section name doesn't
18+
// meet mach-o's requirements. We wouldn't ordinarily build this crate for macOS, but we do so
19+
// because the integration-test crate depends on this crate transitively. See comment in
20+
// test/integration-test/Cargo.toml.
21+
#[cfg_attr(target_arch = "bpf", map)]
1922
pub static mut AYA_LOG_BUF: PerCpuArray<LogBuf> = PerCpuArray::with_max_entries(1, 0);
2023

2124
#[doc(hidden)]
22-
#[map]
25+
// This cfg_attr prevents compilation failures on macOS where the generated section name doesn't
26+
// meet mach-o's requirements. We wouldn't ordinarily build this crate for macOS, but we do so
27+
// because the integration-test crate depends on this crate transitively. See comment in
28+
// test/integration-test/Cargo.toml.
29+
#[cfg_attr(target_arch = "bpf", map)]
2330
pub static mut AYA_LOGS: PerfEventByteArray = PerfEventByteArray::new(0);
2431

2532
#[doc(hidden)]

init/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "init"
3+
version = "0.1.0"
4+
authors = ["Tamir Duberstein <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
anyhow = { workspace = true, features = ["std"] }
10+
nix = { workspace = true, features = ["fs", "mount", "reboot"] }

init/src/main.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use anyhow::Context as _;
2+
3+
#[derive(Debug)]
4+
struct Errors(Vec<anyhow::Error>);
5+
6+
impl std::fmt::Display for Errors {
7+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8+
let Self(errors) = self;
9+
for (i, error) in errors.iter().enumerate() {
10+
if i != 0 {
11+
writeln!(f)?;
12+
}
13+
write!(f, "{:?}", error)?;
14+
}
15+
Ok(())
16+
}
17+
}
18+
19+
impl std::error::Error for Errors {}
20+
21+
fn run() -> anyhow::Result<()> {
22+
const RXRXRX: nix::sys::stat::Mode = nix::sys::stat::Mode::empty()
23+
.union(nix::sys::stat::Mode::S_IRUSR)
24+
.union(nix::sys::stat::Mode::S_IXUSR)
25+
.union(nix::sys::stat::Mode::S_IRGRP)
26+
.union(nix::sys::stat::Mode::S_IXGRP)
27+
.union(nix::sys::stat::Mode::S_IROTH)
28+
.union(nix::sys::stat::Mode::S_IXOTH);
29+
30+
struct Mount {
31+
source: &'static str,
32+
target: &'static str,
33+
fstype: &'static str,
34+
flags: nix::mount::MsFlags,
35+
data: Option<&'static str>,
36+
target_mode: Option<nix::sys::stat::Mode>,
37+
}
38+
39+
for Mount {
40+
source,
41+
target,
42+
fstype,
43+
flags,
44+
data,
45+
target_mode,
46+
} in [
47+
Mount {
48+
source: "proc",
49+
target: "/proc",
50+
fstype: "proc",
51+
flags: nix::mount::MsFlags::empty(),
52+
data: None,
53+
target_mode: Some(RXRXRX),
54+
},
55+
Mount {
56+
source: "sysfs",
57+
target: "/sys",
58+
fstype: "sysfs",
59+
flags: nix::mount::MsFlags::empty(),
60+
data: None,
61+
target_mode: Some(RXRXRX),
62+
},
63+
Mount {
64+
source: "debugfs",
65+
target: "/sys/kernel/debug",
66+
fstype: "debugfs",
67+
flags: nix::mount::MsFlags::empty(),
68+
data: None,
69+
target_mode: None,
70+
},
71+
Mount {
72+
source: "bpffs",
73+
target: "/sys/fs/bpf",
74+
fstype: "bpf",
75+
flags: nix::mount::MsFlags::empty(),
76+
data: None,
77+
target_mode: None,
78+
},
79+
] {
80+
match target_mode {
81+
None => {
82+
// Must exist.
83+
let nix::sys::stat::FileStat { st_mode, .. } = nix::sys::stat::stat(target)
84+
.with_context(|| format!("stat({target}) failed"))?;
85+
let s_flag = nix::sys::stat::SFlag::from_bits_truncate(st_mode);
86+
87+
if !s_flag.contains(nix::sys::stat::SFlag::S_IFDIR) {
88+
anyhow::bail!("{target} is not a directory");
89+
}
90+
}
91+
Some(target_mode) => {
92+
// Must not exist.
93+
nix::unistd::mkdir(target, target_mode)
94+
.with_context(|| format!("mkdir({target}) failed"))?;
95+
}
96+
}
97+
nix::mount::mount(Some(source), target, Some(fstype), flags, data).with_context(|| {
98+
format!("mount({source}, {target}, {fstype}, {flags:?}, {data:?}) failed")
99+
})?;
100+
}
101+
102+
// By contract we run everything in /bin and assume they're rust test binaries.
103+
//
104+
// If the user requested command line arguments, they're named init.arg={}.
105+
106+
// Read kernel parameters from /proc/cmdline. They're space separated on a single line.
107+
let cmdline = std::fs::read_to_string("/proc/cmdline")
108+
.with_context(|| "read_to_string(/proc/cmdline) failed")?;
109+
let args = cmdline
110+
.split_whitespace()
111+
.filter_map(|parameter| {
112+
parameter
113+
.strip_prefix("init.arg=")
114+
.map(std::ffi::OsString::from)
115+
})
116+
.collect::<Vec<_>>();
117+
118+
// Iterate files in /bin.
119+
let read_dir = std::fs::read_dir("/bin").context("read_dir(/bin) failed")?;
120+
let errors = read_dir
121+
.filter_map(|entry| {
122+
match (|| {
123+
let entry = entry.context("read_dir(/bin) failed")?;
124+
let path = entry.path();
125+
let status = std::process::Command::new(&path)
126+
.args(&args)
127+
.status()
128+
.with_context(|| format!("failed to execute {}", path.display()))?;
129+
130+
if status.code() == Some(0) {
131+
Ok(())
132+
} else {
133+
Err(anyhow::anyhow!("{} failed: {status:?}", path.display()))
134+
}
135+
})() {
136+
Ok(()) => None,
137+
Err(err) => Some(err),
138+
}
139+
})
140+
.collect::<Vec<_>>();
141+
if errors.is_empty() {
142+
Ok(())
143+
} else {
144+
Err(Errors(errors).into())
145+
}
146+
}
147+
148+
fn main() {
149+
match run() {
150+
Ok(()) => {
151+
println!("init: success");
152+
}
153+
Err(err) => {
154+
println!("{err:?}");
155+
println!("init: failure");
156+
}
157+
}
158+
let _: std::convert::Infallible =
159+
nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_POWER_OFF)
160+
.expect("reboot failed");
161+
}

0 commit comments

Comments
 (0)