edera_check/recorders/postinstall/
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 = "Postinstall 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_hv_console().boxed(),
39 self.record_hv_debug_info().boxed(),
40 self.record_daemon_logs().boxed(),
41 self.record_cri_logs().boxed(),
42 self.record_storage_logs().boxed(),
43 self.record_kubelet_logs().boxed(),
44 self.record_network_logs().boxed(),
45 self.record_containerd_logs().boxed(),
46 self.record_grub_cfg().boxed(),
47 self.record_kernel_cfg().boxed(),
48 self.record_xen_capabilities().boxed(),
49 self.record_loaded_modules().boxed(),
50 ])
51 .await;
52
53 let mut group_result = Passed;
54 for res in results.iter() {
55 if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
57 group_result = Failed(String::from("group failed"));
58 }
59
60 if matches!(res.result, Errored(_)) {
61 group_result = Errored(String::from("group errored"));
62 }
63 }
64
65 CheckGroupResult {
66 name: NAME.to_string(),
67 result: group_result,
68 results,
69 }
70 }
71
72 async fn run_tool(&self, tool: &str) -> CheckResult {
74 let name = format!("Captured {tool}");
75 let mut tool_args: Vec<String> = tool.split(" ").map(|s| s.to_string()).collect();
76 let cmd = tool_args.remove(0);
77
78 let output = match self
79 .host_executor
80 .spawn_in_host_ns(async move { Command::new(cmd).args(tool_args).output() })
81 .await
82 {
83 Ok(output) => output,
84 Err(e) => return CheckResult::new(&name, Skipped(e.to_string())),
85 };
86
87 let output = match output {
88 Ok(output) => output,
89 Err(e) => return CheckResult::new(&name, Skipped(e.to_string())),
90 };
91
92 if !output.status.success() {
93 let error_message = String::from_utf8_lossy(&output.stderr);
94 return CheckResult::new(&name, Skipped(error_message.to_string()));
95 }
96
97 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
98 CheckResult::new_with_output(&name, Passed, Some(stdout))
99 }
100
101 async fn record_file(&self, file: &Path) -> Option<CheckResult> {
103 let local_file = file.to_path_buf();
104
105 self.host_executor
106 .spawn_in_host_ns(async move {
107 if !local_file.exists() {
108 return None;
109 }
110 let name = format!("Captured {}", local_file.display());
111
112 let bytes = match tokio::fs::read(&local_file).await {
113 Ok(b) => b,
114 Err(e) => {
115 return Some(CheckResult::new(
116 &name,
117 Errored(format!("failed to read {}: {e}", local_file.display())),
118 ));
119 }
120 };
121
122 let content = if bytes.starts_with(&[0x1f, 0x8b]) {
123 let mut decoder = GzDecoder::new(&bytes[..]);
125 let mut s = String::new();
126 decoder.read_to_string(&mut s).map(|_| s)
127 } else {
128 String::from_utf8(bytes)
129 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
130 };
131
132 match content {
133 Ok(c) => Some(CheckResult::new_with_output(&name, Passed, Some(c))),
134 Err(e) => Some(CheckResult::new(
135 &name,
136 Errored(format!("failed to decode {}: {e}", local_file.display())),
137 )),
138 }
139 })
140 .await
141 .unwrap_or_else(|_| panic!("could not record {}", file.display()))
142 }
143
144 pub async fn record_lspci(&self) -> CheckResult {
151 self.run_tool("lspci -vvv").await
152 }
153
154 pub async fn record_dmidecode(&self) -> CheckResult {
161 self.run_tool("dmidecode").await
162 }
163
164 pub async fn record_hv_console(&self) -> CheckResult {
171 self.run_tool("protect-ctl host hv-console").await
172 }
173
174 pub async fn record_hv_debug_info(&self) -> CheckResult {
181 self.run_tool("protect-ctl host hv-debug-info").await
182 }
183
184 pub async fn record_daemon_logs(&self) -> CheckResult {
191 self.run_tool("journalctl -u protect-daemon").await
192 }
193
194 pub async fn record_cri_logs(&self) -> CheckResult {
201 self.run_tool("journalctl -u protect-cri").await
202 }
203
204 pub async fn record_storage_logs(&self) -> CheckResult {
211 self.run_tool("journalctl -u protect-storage").await
212 }
213
214 pub async fn record_network_logs(&self) -> CheckResult {
221 self.run_tool("journalctl -u protect-network").await
222 }
223
224 pub async fn record_containerd_logs(&self) -> CheckResult {
231 self.run_tool("journalctl -u containerd").await
232 }
233
234 pub async fn record_kubelet_logs(&self) -> CheckResult {
241 self.run_tool("journalctl -u kubelet").await
242 }
243
244 pub async fn record_cpuinfo(&self) -> CheckResult {
251 self.record_file(PathBuf::from("/proc/cpuinfo").as_ref())
252 .await
253 .expect("/proc/cpuinfo not found")
254 }
255
256 pub async fn record_cmdline(&self) -> CheckResult {
263 self.record_file(PathBuf::from("/proc/cmdline").as_ref())
264 .await
265 .expect("/proc/cmdline not found")
266 }
267
268 pub async fn record_xen_capabilities(&self) -> CheckResult {
275 self.record_file(PathBuf::from("/sys/hypervisor/properties/capabilities").as_ref())
276 .await
277 .expect("/sys/hypervisor/properties/capabilities not found")
278 }
279
280 pub async fn record_grub_cfg(&self) -> CheckResult {
288 let files = ["/boot/grub2/grub.cfg", "/boot/grub/grub.cfg"];
291
292 for file in files {
293 if let Some(result) = self.record_file(&PathBuf::from(file)).await {
294 return result;
295 }
296 }
297 CheckResult::new(
298 "Record grub config",
299 Skipped(format!("no grub config found in {:?}", files)),
300 )
301 }
302
303 pub async fn record_kernel_cfg(&self) -> CheckResult {
311 let name = "Record kernel config";
312 let Ok(kver) = self.current_kernel_version().await else {
315 return CheckResult::new(name, Errored("failed to find kernel version".to_string()));
316 };
317
318 let files = ["/proc/config.gz", &format!("boot/config-{kver}")];
319
320 for file in files {
321 if let Some(result) = self.record_file(&PathBuf::from(file)).await {
322 return result;
323 }
324 }
325 CheckResult::new(
326 name,
327 Skipped(format!("no kernel config found in {:?}", files)),
328 )
329 }
330
331 pub async fn record_loaded_modules(&self) -> CheckResult {
338 let name = "Record current host kernel loaded modules";
339 match self
340 .host_executor
341 .spawn_in_host_ns(async move { procfs::KernelModules::current() })
342 .await
343 {
344 Ok(Ok(list)) => CheckResult::new_with_output(
345 name,
346 Passed,
347 Some(list.0.into_keys().collect::<Vec<_>>().join("\n")),
348 ),
349 Ok(Err(e)) => CheckResult::new(name, Errored(e.to_string())),
350 Err(e) => CheckResult::new(name, Skipped(e.to_string())),
351 }
352 }
353
354 async fn current_kernel_version(&self) -> Result<String> {
355 self.host_executor
356 .spawn_in_host_ns(async {
357 let output = Command::new("uname")
358 .arg("-r")
359 .output()
360 .expect("Failed to execute command");
361
362 if !output.status.success() {
363 let error_message = String::from_utf8_lossy(&output.stderr);
364 bail!("{}", error_message);
365 }
366
367 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
368 })
369 .await?
370 }
371}
372
373#[async_trait]
374impl CheckGroup for SystemRecorder {
375 fn id(&self) -> &str {
376 GROUP_IDENTIFIER
377 }
378
379 fn name(&self) -> &str {
380 NAME
381 }
382
383 fn description(&self) -> &str {
384 "Record postinstall system information"
385 }
386
387 async fn run(&self) -> CheckGroupResult {
388 self.run_all().await
389 }
390
391 fn category(&self) -> CheckGroupCategory {
392 CheckGroupCategory::Advisory
393 }
394}