Skip to content

Commit a51122a

Browse files
authored
Add categories, byo kernel checker, negative NUMA check (#5)
- Add `byo-kernel` checker to `preinstall` command. This does not run by default, but can be enabled with the explicit `--byo-kernel` flag - Add check group categories - `Required`, `Optional`, and `Advisory`, and change result reporting based on the category. - Recorder should only Skip, not Fail, if it can't find a file that may not be there.
2 parents 423bae4 + b513e01 commit a51122a

11 files changed

Lines changed: 632 additions & 59 deletions

File tree

.github/workflows/release.yaml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Release edera-check
2+
3+
on:
4+
# This workflow runs on every push to main to either open
5+
# a PR or publish the release.
6+
push:
7+
branches:
8+
- main
9+
10+
permissions:
11+
contents: read # Default token to read
12+
13+
jobs:
14+
release-plz-release:
15+
if: ${{ github.repository_owner == 'edera-dev' }}
16+
name: Release-plz release
17+
runs-on: ubuntu-latest
18+
environment: release # Environment for trusted publishing
19+
permissions:
20+
contents: write # Needed to write release artifacts
21+
id-token: write # Needed for trusted publishing
22+
steps:
23+
- name: Harden the runner (Audit all outbound calls)
24+
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
25+
with:
26+
egress-policy: audit
27+
28+
- name: Checkout repository
29+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
with:
31+
fetch-depth: 0
32+
persist-credentials: false
33+
34+
- name: Install Rust toolchain
35+
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # zizmor: ignore[stale-action-refs] -- pinned to stable branch
36+
37+
- name: Install llvm deps
38+
uses: ./.github/actions/install-llvm
39+
40+
- name: generate cultivator token
41+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
42+
id: generate-token
43+
with:
44+
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
45+
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
46+
47+
- name: Run release-plz
48+
uses: release-plz/action@e592230ad39e3ec735402572601fc621aa24355c # v0.5
49+
with:
50+
command: release
51+
env:
52+
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
53+
54+
release-plz-pr:
55+
if: ${{ github.repository_owner == 'edera-dev' }}
56+
name: Release-plz PR
57+
runs-on: ubuntu-latest
58+
environment: release # Environment for trusted publishing
59+
permissions:
60+
contents: write # Needed to write release artifacts
61+
id-token: write # Needed for trusted publishing
62+
pull-requests: write # Needed to create pull requests
63+
concurrency:
64+
group: release-plz-${{ github.ref }}
65+
cancel-in-progress: false
66+
steps:
67+
- name: Harden the runner (Audit all outbound calls)
68+
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
69+
with:
70+
egress-policy: audit
71+
72+
- name: Checkout repository
73+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
74+
with:
75+
fetch-depth: 0
76+
persist-credentials: false
77+
78+
- name: Install Rust toolchain
79+
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # zizmor: ignore[stale-action-refs] -- pinned to stable branch
80+
81+
- name: Install llvm deps
82+
uses: ./.github/actions/install-llvm
83+
84+
- name: generate cultivator token
85+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
86+
id: generate-token
87+
with:
88+
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
89+
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
90+
91+
- name: Run release-plz
92+
uses: release-plz/action@e592230ad39e3ec735402572601fc621aa24355c # v0.5
93+
with:
94+
command: release-pr
95+
env:
96+
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"

src/checkers/byo_kernel.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
use crate::helpers::{
2+
CheckGroup, CheckGroupCategory, CheckGroupResult, CheckResult,
3+
CheckResultValue::{Errored, Failed, Passed},
4+
host_executor::HostNamespaceExecutor,
5+
};
6+
7+
use anyhow::{Result, bail};
8+
use async_trait::async_trait;
9+
use futures::{FutureExt, future::join_all};
10+
use log::debug;
11+
use procfs::{Current, sys::kernel};
12+
use std::{fs, path::PathBuf, process::Command};
13+
14+
const GROUP_IDENTIFIER: &str = "byokernel";
15+
const NAME: &str = "Bring-Your-Own Kernel Checks";
16+
// Modules that the currently running kernel must have as loaded/builtin/loadable
17+
// in order for it to be usable as a BYO kernel
18+
const REQUIRED_MODULES: &[&str] = &[
19+
"nf_tables",
20+
"xen_evtchn",
21+
"xen-privcmd",
22+
"xen-netback",
23+
"xen-pciback",
24+
"xen-blkback",
25+
"xen-gntdev",
26+
"xen-gntalloc",
27+
];
28+
29+
const KVER_FLOOR_PATCH: u16 = 0;
30+
const KVER_FLOOR_MINOR: u8 = 15;
31+
const KVER_FLOOR_MAJOR: u8 = 5;
32+
33+
pub struct BYOKernelChecks {
34+
host_executor: HostNamespaceExecutor,
35+
}
36+
37+
impl BYOKernelChecks {
38+
pub fn new(host_executor: HostNamespaceExecutor) -> Self {
39+
BYOKernelChecks { host_executor }
40+
}
41+
42+
/// Run all the checkers asynchronously, then
43+
/// join and collect the results.
44+
pub async fn run_all(&self) -> CheckGroupResult {
45+
let results = join_all([self.has_modules().boxed(), self.version_is_good().boxed()]).await;
46+
47+
let mut group_result = Passed;
48+
for res in results.iter() {
49+
// Set group result to Failed if we failed and aren't already in an Errored state
50+
if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
51+
group_result = Failed("".into());
52+
}
53+
54+
if matches!(res.result, Errored(_)) {
55+
group_result = Errored("".into());
56+
}
57+
}
58+
59+
CheckGroupResult {
60+
name: NAME.to_string(),
61+
result: group_result,
62+
results,
63+
}
64+
}
65+
66+
async fn version_is_good(&self) -> CheckResult {
67+
let name = String::from("Host Kernel Version Is Good");
68+
let mut result = Passed;
69+
70+
// Get host kernel version
71+
let current = self
72+
.host_executor
73+
.spawn_in_host_ns(async { kernel::Version::current() })
74+
.await
75+
.expect("error spawning in host");
76+
77+
if let Err(e) = current {
78+
return CheckResult::new(&name, Errored(e.to_string()));
79+
}
80+
let current = current.unwrap();
81+
let lowest = kernel::Version::new(KVER_FLOOR_MAJOR, KVER_FLOOR_MINOR, KVER_FLOOR_PATCH);
82+
83+
if current < lowest {
84+
result = Failed(String::from("current kernel version is unsupported"));
85+
}
86+
CheckResult::new(&name, result)
87+
}
88+
89+
async fn has_modules(&self) -> CheckResult {
90+
let name = String::from("Host Has Necessary Modules");
91+
let mut result = Passed;
92+
93+
let required_modules: Vec<String> =
94+
REQUIRED_MODULES.iter().map(|s| s.to_string()).collect();
95+
96+
// Search builtin modules
97+
let remaining = match self.find_builtins(&required_modules).await {
98+
Ok(r) => r,
99+
Err(e) => {
100+
return CheckResult::new(&name, Errored(format!("getting kernel builtins {e}")));
101+
}
102+
};
103+
104+
// Search loaded modules
105+
let remaining = match self.find_loaded(&remaining).await {
106+
Ok(r) => r,
107+
Err(e) => {
108+
return CheckResult::new(&name, Errored(format!("getting kernel modules {e}")));
109+
}
110+
};
111+
112+
// Search loadable modules
113+
let remaining = match self.find_loadable(&remaining).await {
114+
Ok(r) => r,
115+
Err(e) => {
116+
return CheckResult::new(&name, Errored(format!("getting kernel modules {e}")));
117+
}
118+
};
119+
if !remaining.is_empty() {
120+
result = Failed(format!("missing {:?}", remaining))
121+
}
122+
123+
CheckResult::new(&name, result)
124+
}
125+
126+
/// Looks at builtins for kernel_version and compares that to the list of
127+
/// required modules.
128+
/// Returns a vec of everything from required_modules that WAS NOT found in builtins.
129+
async fn find_builtins(&self, required_modules: &[String]) -> Result<Vec<String>> {
130+
let mut modules_to_find: Vec<String> = required_modules.to_owned();
131+
132+
// read host builtins
133+
let builtins = self
134+
.host_executor
135+
.spawn_in_host_ns(async move {
136+
// Get kernel version
137+
let output = Command::new("uname").arg("-r").output()?;
138+
139+
if !output.status.success() {
140+
let error_message = String::from_utf8_lossy(&output.stderr);
141+
bail!("{}", error_message);
142+
}
143+
let kernel_version = String::from_utf8_lossy(&output.stdout).trim().to_string();
144+
let path = PathBuf::from(format!("/lib/modules/{kernel_version}/modules.builtin"));
145+
fs::read_to_string(path).map_err(|e| anyhow::anyhow!(e))
146+
})
147+
.await??;
148+
149+
for builtin in builtins.lines() {
150+
let found = modules_to_find
151+
.iter()
152+
.position(|required| builtin.contains(required));
153+
154+
if let Some(index) = found {
155+
debug!("builtin {}", modules_to_find[index]);
156+
modules_to_find.remove(index);
157+
}
158+
}
159+
160+
Ok(modules_to_find)
161+
}
162+
163+
/// Looks at loaded modules for the current host kernel and compares that to the list of
164+
/// required modules.
165+
/// Returns a vec of everything from required_modules that WAS NOT loaded.
166+
async fn find_loaded(&self, required_modules: &[String]) -> Result<Vec<String>> {
167+
let mut modules_to_find: Vec<String> = required_modules.to_owned();
168+
169+
let modules = self
170+
.host_executor
171+
.spawn_in_host_ns(async move { procfs::KernelModules::current() })
172+
.await?;
173+
174+
let modules = modules.unwrap();
175+
176+
for (name, _) in modules.0.iter() {
177+
let found = modules_to_find.iter().position(|required| required == name);
178+
179+
if let Some(index) = found {
180+
debug!("module {}", modules_to_find[index]);
181+
modules_to_find.remove(index);
182+
}
183+
}
184+
185+
Ok(modules_to_find)
186+
}
187+
188+
/// Looks at not-loaded-but-loadable modules for the current host kernel and compares
189+
/// that to the list of required modules.
190+
/// Returns a vec of everything from required_modules that is available to load (exists in
191+
/// modules.dep) but is NOT currently loaded or builtin.
192+
async fn find_loadable(&self, required_modules: &[String]) -> Result<Vec<String>> {
193+
let mut modules_to_find: Vec<String> = required_modules.to_owned();
194+
let dep_file = self
195+
.host_executor
196+
.spawn_in_host_ns(async move {
197+
let output = Command::new("uname").arg("-r").output()?;
198+
if !output.status.success() {
199+
let error_message = String::from_utf8_lossy(&output.stderr);
200+
bail!("{}", error_message);
201+
}
202+
let kernel_version = String::from_utf8_lossy(&output.stdout).trim().to_string();
203+
let path = PathBuf::from(format!("/lib/modules/{kernel_version}/modules.dep"));
204+
fs::read_to_string(path).map_err(|e| anyhow::anyhow!(e))
205+
})
206+
.await??;
207+
208+
for line in dep_file.lines() {
209+
let module_path = line.split(':').next().unwrap_or("");
210+
let found = modules_to_find
211+
.iter()
212+
.position(|required| module_path.contains(required.as_str()));
213+
if let Some(index) = found {
214+
debug!("available {}", modules_to_find[index]);
215+
modules_to_find.remove(index);
216+
}
217+
}
218+
Ok(modules_to_find)
219+
}
220+
}
221+
222+
#[async_trait]
223+
impl CheckGroup for BYOKernelChecks {
224+
fn id(&self) -> &str {
225+
GROUP_IDENTIFIER
226+
}
227+
228+
fn name(&self) -> &str {
229+
NAME
230+
}
231+
232+
fn description(&self) -> &str {
233+
"Bring Your Own Kernel requirement checks"
234+
}
235+
236+
async fn run(&self) -> CheckGroupResult {
237+
self.run_all().await
238+
}
239+
240+
fn category(&self) -> CheckGroupCategory {
241+
CheckGroupCategory::Optional(
242+
"Active kernel not sufficient for Bring-Your-Own-Kernel support".into(),
243+
)
244+
}
245+
}

src/checkers/iommu.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::helpers::{
2-
CheckGroup, CheckGroupResult, CheckResult,
2+
CheckGroup, CheckGroupCategory, CheckGroupResult, CheckResult,
33
CheckResultValue::{Errored, Failed, Passed},
44
host_executor::HostNamespaceExecutor,
55
};
@@ -30,11 +30,11 @@ impl IOMMUChecks {
3030
for res in results.iter() {
3131
// Set group result to Failed if we failed and aren't already in an Errored state
3232
if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
33-
group_result = Failed(String::from("group failed"));
33+
group_result = Failed("".into());
3434
}
3535

3636
if matches!(res.result, Errored(_)) {
37-
group_result = Errored(String::from("group errored"));
37+
group_result = Errored("".into());
3838
}
3939
}
4040

@@ -103,4 +103,10 @@ impl CheckGroup for IOMMUChecks {
103103
async fn run(&self) -> CheckGroupResult {
104104
self.run_all().await
105105
}
106+
107+
fn category(&self) -> CheckGroupCategory {
108+
CheckGroupCategory::Optional(
109+
"PVH and some host device support not available on this system".into(),
110+
)
111+
}
106112
}

0 commit comments

Comments
 (0)