edera_check/recorders/preinstall/
system.rs1use anyhow::{Result, bail};
2use async_trait::async_trait;
3use flate2::read::GzDecoder;
4use futures::{FutureExt, future::join_all};
5use procfs::Current;
6use std::{
7 io::Read,
8 path::{Path, PathBuf},
9 process::Command,
10};
11
12use crate::helpers::{
13 CheckGroup, CheckGroupCategory, CheckGroupResult, CheckResult,
14 CheckResultValue::{Errored, Failed, Passed, Skipped},
15 host_executor::HostNamespaceExecutor,
16};
17
18const GROUP_IDENTIFIER: &str = "sysinfo";
19const NAME: &str = "System Info Recorder";
20
21pub struct SystemRecorder {
22 host_executor: HostNamespaceExecutor,
23}
24
25impl SystemRecorder {
26 pub fn new(host_executor: HostNamespaceExecutor) -> Self {
27 SystemRecorder { host_executor }
28 }
29
30 pub async fn run_all(&self) -> CheckGroupResult {
33 let results = join_all([
34 self.record_lspci().boxed(),
35 self.record_dmidecode().boxed(),
36 self.record_cpuinfo().boxed(),
37 self.record_cmdline().boxed(),
38 self.record_grub_cfg().boxed(),
39 self.record_kernel_cfg().boxed(),
40 self.record_loaded_modules().boxed(),
41 ])
42 .await;
43
44 let mut group_result = Passed;
45 for res in results.iter() {
46 if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
48 group_result = Failed(String::from("group failed"));
49 }
50
51 if matches!(res.result, Errored(_)) {
52 group_result = Errored(String::from("group errored"));
53 }
54 }
55
56 CheckGroupResult {
57 name: NAME.to_string(),
58 result: group_result,
59 results,
60 }
61 }
62
63 async fn run_tool(&self, tool: &str) -> CheckResult {
65 let name = format!("Captured {tool}");
66 let mut tool_args: Vec<String> = tool.split(" ").map(|s| s.to_string()).collect();
67 let cmd = tool_args.remove(0);
68
69 let output = match self
70 .host_executor
71 .spawn_in_host_ns(async move { Command::new(cmd).args(tool_args).output() })
72 .await
73 {
74 Ok(output) => output,
75 Err(e) => return CheckResult::new(&name, Skipped(e.to_string())),
76 };
77
78 let output = match output {
79 Ok(output) => output,
80 Err(e) => return CheckResult::new(&name, Skipped(e.to_string())),
81 };
82
83 if !output.status.success() {
84 let error_message = String::from_utf8_lossy(&output.stderr);
85 return CheckResult::new(&name, Skipped(error_message.to_string()));
86 }
87
88 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
89 CheckResult::new_with_output(&name, Passed, Some(stdout))
90 }
91
92 async fn record_file(&self, file: &Path) -> Option<CheckResult> {
94 let local_file = file.to_path_buf();
95
96 self.host_executor
97 .spawn_in_host_ns(async move {
98 if !local_file.exists() {
99 return None;
100 }
101 let name = format!("Captured {}", local_file.display());
102
103 let bytes = match tokio::fs::read(&local_file).await {
104 Ok(b) => b,
105 Err(e) => {
106 return Some(CheckResult::new(
107 &name,
108 Errored(format!("failed to read {}: {e}", local_file.display())),
109 ));
110 }
111 };
112
113 let content = if bytes.starts_with(&[0x1f, 0x8b]) {
114 let mut decoder = GzDecoder::new(&bytes[..]);
116 let mut s = String::new();
117 decoder.read_to_string(&mut s).map(|_| s)
118 } else {
119 String::from_utf8(bytes)
120 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
121 };
122
123 match content {
124 Ok(c) => Some(CheckResult::new_with_output(&name, Passed, Some(c))),
125 Err(e) => Some(CheckResult::new(
126 &name,
127 Errored(format!("failed to decode {}: {e}", local_file.display())),
128 )),
129 }
130 })
131 .await
132 .unwrap_or_else(|_| panic!("could not record {}", file.display()))
133 }
134
135 pub async fn record_lspci(&self) -> CheckResult {
142 self.run_tool("lspci -vvv").await
143 }
144
145 pub async fn record_dmidecode(&self) -> CheckResult {
152 self.run_tool("dmidecode").await
153 }
154
155 pub async fn record_cpuinfo(&self) -> CheckResult {
162 self.record_file(PathBuf::from("/proc/cpuinfo").as_ref())
163 .await
164 .expect("/proc/cpuinfo not found")
165 }
166
167 pub async fn record_cmdline(&self) -> CheckResult {
174 self.record_file(PathBuf::from("/proc/cmdline").as_ref())
175 .await
176 .expect("/proc/cmdline not found")
177 }
178
179 pub async fn record_grub_cfg(&self) -> CheckResult {
187 let files = ["/boot/grub2/grub.cfg", "/boot/grub/grub.cfg"];
190
191 for file in files {
192 if let Some(result) = self.record_file(&PathBuf::from(file)).await {
193 return result;
194 }
195 }
196 CheckResult::new(
197 "Record grub config",
198 Skipped(format!("no grub config found in {:?}", files)),
199 )
200 }
201
202 pub async fn record_kernel_cfg(&self) -> CheckResult {
210 let name = "Record kernel config";
211 let Ok(kver) = self.current_kernel_version().await else {
214 return CheckResult::new(name, Errored("failed to find kernel version".to_string()));
215 };
216
217 let files = ["/proc/config.gz", &format!("boot/config-{kver}")];
218
219 for file in files {
220 if let Some(result) = self.record_file(&PathBuf::from(file)).await {
221 return result;
222 }
223 }
224 CheckResult::new(
225 name,
226 Skipped(format!("no kernel config found in {:?}", files)),
227 )
228 }
229
230 pub async fn record_loaded_modules(&self) -> CheckResult {
237 let name = "Record current host kernel loaded modules";
238 match self
239 .host_executor
240 .spawn_in_host_ns(async move { procfs::KernelModules::current() })
241 .await
242 {
243 Ok(Ok(list)) => CheckResult::new_with_output(
244 name,
245 Passed,
246 Some(list.0.into_keys().collect::<Vec<_>>().join("\n")),
247 ),
248 Ok(Err(e)) => CheckResult::new(name, Errored(e.to_string())),
249 Err(e) => CheckResult::new(name, Skipped(e.to_string())),
250 }
251 }
252
253 async fn current_kernel_version(&self) -> Result<String> {
254 self.host_executor
255 .spawn_in_host_ns(async {
256 let output = Command::new("uname")
257 .arg("-r")
258 .output()
259 .expect("Failed to execute command");
260
261 if !output.status.success() {
262 let error_message = String::from_utf8_lossy(&output.stderr);
263 bail!("{}", error_message);
264 }
265
266 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
267 })
268 .await?
269 }
270}
271
272#[async_trait]
273impl CheckGroup for SystemRecorder {
274 fn id(&self) -> &str {
275 GROUP_IDENTIFIER
276 }
277
278 fn name(&self) -> &str {
279 NAME
280 }
281
282 fn description(&self) -> &str {
283 "Record system information for reporting purposes"
284 }
285
286 async fn run(&self) -> CheckGroupResult {
287 self.run_all().await
288 }
289
290 fn category(&self) -> CheckGroupCategory {
291 CheckGroupCategory::Advisory
292 }
293}