Skip to content

Commit 6cd67c0

Browse files
committed
feat: Refactor init into test-distro
The init module contains a small init system for running our integration tests against a kernel. While we don't need a full-blown linux distro, we do need some utilities. Once such utility is `modprobe` which allows us to load kernel modules. Rather than create a new module for this utility, I've instead refactored `init` into `test-distro` which is a module that contains multiple binaries. The xtask code has been adjusted to ensure these binaries are inserted into the correct places in our cpio archive, as well as bringing in the kernel modules. Signed-off-by: Dave Tucker <[email protected]>
1 parent e82253c commit 6cd67c0

File tree

13 files changed

+560
-69
lines changed

13 files changed

+560
-69
lines changed

.github/scripts/find_kernels.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import glob
5+
import sys
6+
from typing import List
7+
8+
def find_kernels(directory: str) -> List[str]:
9+
return glob.glob(f"{directory}/**/vmlinuz-*", recursive=True)
10+
11+
def find_modules_directory(directory: str, kernel: str) -> str:
12+
matches = glob.glob(f"{directory}/**/modules/{kernel}", recursive=True)
13+
if len(matches) == 0:
14+
print(f"ERROR! No modules directory found for kernel {kernel}")
15+
sys.exit(1)
16+
return matches[0]
17+
18+
def main() -> None:
19+
images = find_kernels('test/.tmp')
20+
modules = []
21+
22+
for image in images:
23+
image_name = os.path.basename(image).replace('vmlinuz-', '')
24+
module_dir = find_modules_directory('test/.tmp', image_name)
25+
modules.append(module_dir[0])
26+
27+
if len(images) != len(modules):
28+
print(f"IMAGES={images}")
29+
print(f"MODULES={modules}")
30+
print("ERROR! len images != len modules")
31+
sys.exit(1)
32+
33+
args = ' '.join(f"-i {image} -m {module}" for image, module in zip(images, modules))
34+
print(args)
35+
36+
if __name__ == "__main__":
37+
main()

.github/workflows/ci.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ jobs:
224224
run: |
225225
set -euxo pipefail
226226
sudo apt update
227-
sudo apt -y install lynx qemu-system-{arm,x86}
227+
sudo apt -y install lynx qemu-system-{arm,x86} musl-tools
228228
echo /usr/lib/llvm-15/bin >> $GITHUB_PATH
229229
230230
- name: Install prerequisites
@@ -236,6 +236,13 @@ jobs:
236236
# The tar shipped on macOS doesn't support --wildcards, so we need GNU tar.
237237
#
238238
# The clang shipped on macOS doesn't support BPF, so we need LLVM from brew.
239+
#
240+
# We need a musl C toolchain to compile our `test-distro` since some of
241+
# our dependencies have build scripts that compile C code (i.e xz2).
242+
# This is provided by `brew install filosottile/musl-cross/musl-cross`.
243+
# Setting the linker for x86_64-unknown-linux-musl to x86_64-linux-musl-gcc
244+
# breaks compilation on linux. The simplest option to solve this is to
245+
# symlink x86_64-linux-musl-gcc to musl-gcc.
239246
run: |
240247
set -euxo pipefail
241248
brew update
@@ -246,6 +253,8 @@ jobs:
246253
echo $(brew --prefix curl)/bin >> $GITHUB_PATH
247254
echo $(brew --prefix gnu-tar)/libexec/gnubin >> $GITHUB_PATH
248255
echo $(brew --prefix llvm)/bin >> $GITHUB_PATH
256+
brew install filosottile/musl-cross/musl-cross
257+
ln -s "$(brew --prefix musl-cross)/bin/x86_64-linux-musl-gcc" /usr/local/bin/musl-gcc
249258
250259
- uses: dtolnay/rust-toolchain@nightly
251260
with:
@@ -303,8 +312,15 @@ jobs:
303312
- name: Extract debian kernels
304313
run: |
305314
set -euxo pipefail
315+
# Remove old images and modules.
316+
rm -rf test/.tmp/boot test/.tmp/lib
317+
# The wildcard '**/boot/*' extracts kernel images and config.
318+
# The wildcard '**/modules/*' extracts kernel modules.
319+
# Modules are required since not all parts of the kernel we want to
320+
# test are built-in.
306321
find test/.tmp -name '*.deb' -print0 | xargs -t -0 -I {} \
307-
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' --file -"
322+
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp \
323+
--wildcards --extract '**/boot/*' '**/modules/*' --file -"
308324
309325
- name: Run local integration tests
310326
if: runner.os == 'Linux'
@@ -313,8 +329,10 @@ jobs:
313329
- name: Run virtualized integration tests
314330
run: |
315331
set -euxo pipefail
316-
find test/.tmp -name 'vmlinuz-*' -print0 | xargs -t -0 \
317-
cargo xtask integration-test vm --cache-dir test/.tmp --github-api-token ${{ secrets.GITHUB_TOKEN }}
332+
ARGS=$(./.github/scripts/find_kernels.py)
333+
cargo xtask integration-test vm --cache-dir test/.tmp \
334+
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
335+
${ARGS}
318336
319337
# Provides a single status check for the entire build workflow.
320338
# This is used for merge automation, like Mergify, since GH actions

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
"aya-log-parser",
88
"aya-obj",
99
"aya-tool",
10-
"init",
10+
"test-distro",
1111
"test/integration-common",
1212
"test/integration-test",
1313
"xtask",
@@ -33,7 +33,7 @@ default-members = [
3333
"aya-log-parser",
3434
"aya-obj",
3535
"aya-tool",
36-
"init",
36+
"test-distro",
3737
"test/integration-common",
3838
# test/integration-test is omitted; including it in this list causes `cargo test` to run its
3939
# tests, and that doesn't work unless they've been built with `cargo xtask`.
@@ -73,6 +73,7 @@ diff = { version = "0.1.13", default-features = false }
7373
env_logger = { version = "0.11", default-features = false }
7474
epoll = { version = "4.3.3", default-features = false }
7575
futures = { version = "0.3.28", default-features = false }
76+
glob = { version = "0.3.0", default-features = false }
7677
hashbrown = { version = "0.15.0", default-features = false }
7778
indoc = { version = "2.0", default-features = false }
7879
libc = { version = "0.2.105", default-features = false }
@@ -99,8 +100,10 @@ test-log = { version = "0.2.13", default-features = false }
99100
testing_logger = { version = "0.1.1", default-features = false }
100101
thiserror = { version = "2.0.3", default-features = false }
101102
tokio = { version = "1.24.0", default-features = false }
103+
walkdir = { version = "2", default-features = false }
102104
which = { version = "7.0.0", default-features = false }
103105
xdpilone = { version = "1.0.5", default-features = false }
106+
xz2 = { version = "0.1.7", default-features = false }
104107

105108
[profile.release.package.integration-ebpf]
106109
debug = 2

init/Cargo.toml

Lines changed: 0 additions & 13 deletions
This file was deleted.

test-distro/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[package]
2+
name = "test-distro"
3+
version = "0.1.0"
4+
publish = false
5+
authors.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
homepage.workspace = true
9+
edition.workspace = true
10+
11+
[[bin]]
12+
name = "init"
13+
path = "src/init.rs"
14+
15+
[[bin]]
16+
name = "modprobe"
17+
path = "src/modprobe.rs"
18+
19+
[[bin]]
20+
name = "depmod"
21+
path = "src/depmod.rs"
22+
23+
[dependencies]
24+
anyhow = { workspace = true, features = ["std"] }
25+
object = { workspace = true, features = ["elf", "read_core", "std"] }
26+
clap = { workspace = true, default-features = true, features = ["derive"] }
27+
nix = { workspace = true, features = [
28+
"user",
29+
"fs",
30+
"mount",
31+
"reboot",
32+
"kmod",
33+
"feature",
34+
] }
35+
glob = { workspace = true }
36+
xz2 = { workspace = true }
37+
walkdir = { workspace = true }

test-distro/src/depmod.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! depmod is used to build the modules.alias file to assist with loading
2+
//! kernel modules.
3+
//!
4+
//! This implementation is incredibly naive and is only designed to work within
5+
//! the constraints of the test environment. Not for production use.
6+
7+
use std::{
8+
fs::File,
9+
io::{BufWriter, Read, Write as _},
10+
path::PathBuf,
11+
};
12+
13+
use anyhow::Context as _;
14+
use clap::Parser;
15+
use object::{Object, ObjectSection, ObjectSymbol};
16+
use test_distro::resolve_modules_dir;
17+
use walkdir::WalkDir;
18+
use xz2::read::XzDecoder;
19+
20+
#[derive(Parser)]
21+
struct Args {
22+
#[clap(long, short)]
23+
base_dir: Option<PathBuf>,
24+
}
25+
26+
fn main() -> anyhow::Result<()> {
27+
let Args { base_dir } = Parser::parse();
28+
29+
let modules_dir = if let Some(base_dir) = base_dir {
30+
base_dir
31+
} else {
32+
resolve_modules_dir().context("Failed to resolve modules dir")?
33+
};
34+
35+
let modules_alias = modules_dir.join("modules.alias");
36+
let f = std::fs::OpenOptions::new()
37+
.create(true)
38+
.write(true)
39+
.truncate(true)
40+
.open(&modules_alias)
41+
.with_context(|| format!("failed to open: {}", modules_alias.display()))?;
42+
let mut output = BufWriter::new(&f);
43+
for entry in WalkDir::new(modules_dir) {
44+
let entry = entry.context("failed to read entry in walkdir")?;
45+
if entry.file_type().is_file() {
46+
let path = entry.path();
47+
if let Some(extension) = path.extension() {
48+
if extension != "ko" && extension != "xz" {
49+
continue;
50+
}
51+
let module_name = path
52+
.file_stem()
53+
.ok_or(anyhow::anyhow!("failed to get file stem"))?
54+
.to_os_string()
55+
.into_string()
56+
.map_err(|_| anyhow::anyhow!("failed to convert to string"))?
57+
.replace(".ko", "");
58+
let mut f = File::open(path)
59+
.with_context(|| format!("failed to open: {}", path.display()))?;
60+
let stat = f
61+
.metadata()
62+
.with_context(|| format!("Failed to get metadata for {}", path.display()))?;
63+
if extension == "xz" {
64+
let mut decoder = XzDecoder::new(f);
65+
let mut decompressed = Vec::with_capacity(stat.len() as usize * 2);
66+
decoder.read_to_end(&mut decompressed)?;
67+
read_aliases_from_module(&decompressed, &module_name, &mut output)
68+
} else {
69+
let mut buf = Vec::with_capacity(stat.len() as usize);
70+
f.read_to_end(&mut buf)
71+
.with_context(|| format!("Failed to read: {}", path.display()))?;
72+
read_aliases_from_module(&buf, &module_name, &mut output)
73+
}
74+
.with_context(|| {
75+
format!("Failed to read aliases from module {}", path.display())
76+
})?;
77+
}
78+
}
79+
}
80+
81+
Ok(())
82+
}
83+
84+
fn read_aliases_from_module(
85+
contents: &[u8],
86+
module_name: &str,
87+
output: &mut BufWriter<&File>,
88+
) -> Result<(), anyhow::Error> {
89+
let obj = object::read::File::parse(contents).context("not an object file")?;
90+
91+
let (section_idx, data) = obj
92+
.sections()
93+
.filter_map(|s| {
94+
if let Ok(name) = s.name() {
95+
if name == ".modinfo" {
96+
if let Ok(data) = s.data() {
97+
return Some((s.index(), data));
98+
}
99+
}
100+
}
101+
None
102+
})
103+
.next()
104+
.context("no .modinfo section")?;
105+
106+
obj.symbols()
107+
.try_for_each(|s| -> Result<(), anyhow::Error> {
108+
let name = s.name().context("failed to get symbol name")?;
109+
if name.contains("alias") && s.section_index() == Some(section_idx) {
110+
let start = s.address() as usize;
111+
let end = start + s.size() as usize;
112+
let sym_data = &data[start..end];
113+
let cstr = std::ffi::CStr::from_bytes_with_nul(sym_data)
114+
.context("failed to convert to cstr")?;
115+
let sym_str = cstr.to_str().context("failed to convert to str")?;
116+
let alias = sym_str.replace("alias=", "");
117+
writeln!(output, "alias {} {}", alias, module_name).expect("write");
118+
}
119+
Ok(())
120+
})?;
121+
Ok(())
122+
}

init/src/main.rs renamed to test-distro/src/init.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ fn run() -> anyhow::Result<()> {
5757
data: None,
5858
target_mode: Some(RXRXRX),
5959
},
60+
Mount {
61+
source: "dev",
62+
target: "/dev",
63+
fstype: "devtmpfs",
64+
flags: nix::mount::MsFlags::empty(),
65+
data: None,
66+
target_mode: None,
67+
},
6068
Mount {
6169
source: "sysfs",
6270
target: "/sys",

test-distro/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::Context as _;
4+
use nix::sys::utsname::uname;
5+
6+
/// Kernel modules are in `/lib/modules`.
7+
/// They may be in the root of this directory,
8+
/// or in subdirectory named after the kernel release.
9+
pub fn resolve_modules_dir() -> anyhow::Result<PathBuf> {
10+
let modules_dir = PathBuf::from("/lib/modules");
11+
let stat = modules_dir
12+
.metadata()
13+
.with_context(|| format!("{} doesn't exist", modules_dir.display()))?;
14+
if stat.is_dir() {
15+
return Ok(modules_dir);
16+
}
17+
18+
let utsname = uname().context("failed to get kernel release")?;
19+
let release = utsname.release();
20+
let modules_dir = modules_dir.join(release);
21+
let stat = modules_dir
22+
.metadata()
23+
.with_context(|| format!("{} doesn't exist", modules_dir.display()))?;
24+
anyhow::ensure!(
25+
stat.is_dir(),
26+
"{} is not a directory",
27+
modules_dir.display()
28+
);
29+
Ok(modules_dir)
30+
}

0 commit comments

Comments
 (0)