Skip to content

Commit 11ec62c

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 3edc36a commit 11ec62c

File tree

13 files changed

+568
-77
lines changed

13 files changed

+568
-77
lines changed

.github/scripts/find_kernels.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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) != 1:
14+
raise Exception(f"Expected to find exactly one modules directory. Found {len(matches)}.")
15+
return matches[0]
16+
17+
def main() -> None:
18+
try:
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)
26+
27+
args = ' '.join(f"{image}:{module}" for image, module in zip(images, modules))
28+
print(args)
29+
except Exception as e:
30+
print(e)
31+
sys.exit(1)
32+
33+
if __name__ == "__main__":
34+
main()

.github/workflows/ci.yml

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ jobs:
5151
if: github.event_name != 'pull_request' && github.repository_owner == 'aya-rs'
5252
with:
5353
branch: create-pull-request/public-api
54-
commit-message: 'public-api: regenerate'
55-
title: 'public-api: regenerate'
54+
commit-message: "public-api: regenerate"
55+
title: "public-api: regenerate"
5656
body: |
5757
**Automated changes**
5858
@@ -228,7 +228,7 @@ jobs:
228228
run: |
229229
set -euxo pipefail
230230
sudo apt update
231-
sudo apt -y install lynx qemu-system-{arm,x86}
231+
sudo apt -y install lynx qemu-system-{arm,x86} musl-tools
232232
echo /usr/lib/llvm-15/bin >> $GITHUB_PATH
233233
234234
- name: Install prerequisites
@@ -240,6 +240,10 @@ jobs:
240240
# The tar shipped on macOS doesn't support --wildcards, so we need GNU tar.
241241
#
242242
# The clang shipped on macOS doesn't support BPF, so we need LLVM from brew.
243+
#
244+
# We need a musl C toolchain to compile our `test-distro` since some of
245+
# our dependencies have build scripts that compile C code (i.e xz2).
246+
# This is provided by `brew install filosottile/musl-cross/musl-cross`.
243247
run: |
244248
set -euxo pipefail
245249
brew update
@@ -250,6 +254,8 @@ jobs:
250254
echo $(brew --prefix curl)/bin >> $GITHUB_PATH
251255
echo $(brew --prefix gnu-tar)/libexec/gnubin >> $GITHUB_PATH
252256
echo $(brew --prefix llvm)/bin >> $GITHUB_PATH
257+
brew install filosottile/musl-cross/musl-cross
258+
ln -s "$(brew --prefix musl-cross)/bin/x86_64-linux-musl-gcc" /usr/local/bin/musl-gcc
253259
254260
- uses: dtolnay/rust-toolchain@nightly
255261
with:
@@ -302,21 +308,46 @@ jobs:
302308
# TODO: enable tests on kernels before 6.0.
303309
run: .github/scripts/download_kernel_images.sh test/.tmp/debian-kernels/amd64 amd64 6.1 6.10
304310

311+
- name: Cleanup stale kernels and modules
312+
run: |
313+
set -euxo pipefail
314+
rm -rf test/.tmp/boot test/.tmp/lib
315+
305316
- name: Extract debian kernels
306317
run: |
307318
set -euxo pipefail
319+
# The wildcard '**/boot/*' extracts kernel images and config.
320+
# The wildcard '**/modules/*' extracts kernel modules.
321+
# Modules are required since not all parts of the kernel we want to
322+
# test are built-in.
308323
find test/.tmp -name '*.deb' -print0 | xargs -t -0 -I {} \
309-
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' --file -"
324+
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp \
325+
--wildcards --extract '**/boot/*' '**/modules/*' --file -"
310326
311327
- name: Run local integration tests
312328
if: runner.os == 'Linux'
313329
run: cargo xtask integration-test local
314330

315331
- name: Run virtualized integration tests
332+
if: runner.os == 'Linux'
333+
run: |
334+
set -euxo pipefail
335+
ARGS=$(./.github/scripts/find_kernels.py)
336+
cargo xtask integration-test vm --cache-dir test/.tmp \
337+
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
338+
${ARGS}
339+
340+
- name: Run virtualized integration tests
341+
if: runner.os == 'macOS'
342+
env:
343+
# This sets the linker to the one installed by FiloSottile/musl-cross.
344+
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc
316345
run: |
317346
set -euxo pipefail
318-
find test/.tmp -name 'vmlinuz-*' -print0 | xargs -t -0 \
319-
cargo xtask integration-test vm --cache-dir test/.tmp --github-api-token ${{ secrets.GITHUB_TOKEN }}
347+
ARGS=$(./.github/scripts/find_kernels.py)
348+
cargo xtask integration-test vm --cache-dir test/.tmp \
349+
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
350+
${ARGS}
320351
321352
# Provides a single status check for the entire build workflow.
322353
# 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`.
@@ -74,6 +74,7 @@ diff = { version = "0.1.13", default-features = false }
7474
env_logger = { version = "0.11", default-features = false }
7575
epoll = { version = "4.3.3", default-features = false }
7676
futures = { version = "0.3.28", default-features = false }
77+
glob = { version = "0.3.0", default-features = false }
7778
hashbrown = { version = "0.15.0", default-features = false }
7879
indoc = { version = "2.0", default-features = false }
7980
libc = { version = "0.2.105", default-features = false }
@@ -101,8 +102,10 @@ test-log = { version = "0.2.13", default-features = false }
101102
testing_logger = { version = "0.1.1", default-features = false }
102103
thiserror = { version = "2.0.3", default-features = false }
103104
tokio = { version = "1.24.0", default-features = false }
105+
walkdir = { version = "2", default-features = false }
104106
which = { version = "7.0.0", default-features = false }
105107
xdpilone = { version = "1.0.5", default-features = false }
108+
xz2 = { version = "0.1.7", default-features = false }
106109

107110
[workspace.lints.rust]
108111
unused-extern-crates = "warn"

init/Cargo.toml

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

test-distro/Cargo.toml

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

test-distro/src/depmod.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 _, anyhow};
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+
48+
if path
49+
.extension()
50+
.is_none_or(|ext| ext != "ko" && ext != "xz")
51+
{
52+
continue;
53+
}
54+
55+
let module_name = path
56+
.file_name()
57+
.ok_or_else(|| anyhow!("{} does not have a file name", path.display()))?
58+
.to_str()
59+
.ok_or_else(|| anyhow!("{} is not valid utf-8", path.display()))?;
60+
61+
let (module_name, compressed) =
62+
if let Some(module_name) = module_name.strip_suffix(".xz") {
63+
(module_name, true)
64+
} else {
65+
(module_name, false)
66+
};
67+
68+
let module_name = module_name
69+
.strip_suffix(".ko")
70+
.ok_or_else(|| anyhow!("{} does not end in .ko", path.display()))?;
71+
72+
let mut f =
73+
File::open(path).with_context(|| format!("failed to open: {}", path.display()))?;
74+
let stat = f
75+
.metadata()
76+
.with_context(|| format!("failed to get metadata for {}", path.display()))?;
77+
78+
if compressed {
79+
let mut decoder = XzDecoder::new(f);
80+
// We don't know the size of the decompressed data, so we assume it's
81+
// no more than twice the size of the compressed data.
82+
let mut decompressed = Vec::with_capacity(stat.len() as usize * 2);
83+
decoder.read_to_end(&mut decompressed)?;
84+
read_aliases_from_module(&decompressed, module_name, &mut output)
85+
} else {
86+
let mut buf = Vec::with_capacity(stat.len() as usize);
87+
f.read_to_end(&mut buf)
88+
.with_context(|| format!("failed to read: {}", path.display()))?;
89+
read_aliases_from_module(&buf, module_name, &mut output)
90+
}
91+
.with_context(|| format!("failed to read aliases from module {}", path.display()))?;
92+
}
93+
}
94+
Ok(())
95+
}
96+
97+
fn read_aliases_from_module(
98+
contents: &[u8],
99+
module_name: &str,
100+
output: &mut BufWriter<&File>,
101+
) -> Result<(), anyhow::Error> {
102+
let obj = object::read::File::parse(contents).context("failed to parse")?;
103+
104+
let (section_idx, data) = obj
105+
.sections()
106+
.find_map(|s| {
107+
s.name()
108+
.is_ok_and(|name| name == ".modinfo")
109+
.then(|| s.data().ok())
110+
.flatten()
111+
.map(|data| (s.index(), data))
112+
})
113+
.context("no .modinfo section")?;
114+
115+
for s in obj.symbols() {
116+
let name = s.name().context("failed to get symbol name")?;
117+
if name.contains("alias") && s.section_index() == Some(section_idx) {
118+
let start = s.address() as usize;
119+
let end = start + s.size() as usize;
120+
let sym_data = &data[start..end];
121+
let cstr = std::ffi::CStr::from_bytes_with_nul(sym_data)
122+
.context("failed to convert to cstr")?;
123+
let sym_str = cstr.to_str().context("failed to convert to str")?;
124+
let alias = sym_str
125+
.strip_prefix("alias=")
126+
.context("failed to strip prefix")?;
127+
writeln!(output, "alias {} {}", alias, module_name).expect("write");
128+
}
129+
}
130+
Ok(())
131+
}

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!("stat(): {}", modules_dir.display()))?;
14+
if stat.is_dir() {
15+
return Ok(modules_dir);
16+
}
17+
18+
let utsname = uname().context("uname()")?;
19+
let release = utsname.release();
20+
let modules_dir = modules_dir.join(release);
21+
let stat = modules_dir
22+
.metadata()
23+
.with_context(|| format!("stat(): {}", 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)