Skip to main content

edera_check/checkers/preinstall/
system.rs

1use crate::helpers::{
2    CheckGroup, CheckGroupCategory, CheckGroupResult, CheckResult,
3    CheckResultValue::{Errored, Failed, Passed},
4    host_executor::HostNamespaceExecutor,
5};
6
7use async_trait::async_trait;
8use futures::{FutureExt, future::join_all};
9use log::debug;
10use sysinfo::{Disks, System};
11
12const GROUP_IDENTIFIER: &str = "system";
13const NAME: &str = "System Checks";
14const MINIMUM_MEMORY: u64 = 4 * 1024 * 1024 * 1024; // 4GB
15const MINIMUM_DISK: u64 = 20 * 1024 * 1024 * 1024; // 20GB
16
17pub struct SystemChecks {
18    host_executor: HostNamespaceExecutor,
19}
20
21impl SystemChecks {
22    pub fn new(host_executor: HostNamespaceExecutor) -> Self {
23        SystemChecks { host_executor }
24    }
25    pub async fn run_all(&self) -> CheckGroupResult {
26        let results = join_all([
27            self.enough_memory().boxed(),
28            self.enough_disk().boxed(),
29            self.has_nft_bin().boxed(),
30            self.has_package_manager_bin().boxed(),
31            self.has_grub_mkconfig_bin().boxed(),
32            self.has_service_manager_bin().boxed(),
33            self.has_linux_util_bins(&["tar", "grep"]).boxed(),
34        ])
35        .await;
36
37        let mut group_result = Passed;
38        for res in results.iter() {
39            // Set group result to Failed if we failed and aren't already in an Errored state
40            if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
41                group_result = Failed("".into());
42            }
43
44            if matches!(res.result, Errored(_)) {
45                group_result = Errored("".into());
46            }
47        }
48
49        CheckGroupResult {
50            name: NAME.to_string(),
51            result: group_result,
52            results,
53        }
54    }
55
56    /// Checks that the `nft` binary, typically from the `nftables` package,
57    /// is in PATH. Currently, the installer and `protect-network` rely on this.
58    pub async fn has_nft_bin(&self) -> CheckResult {
59        let name = String::from("'nft' Binary Present");
60        let found = self
61            .host_executor
62            .spawn_in_host_ns(async {
63                std::process::Command::new("nft")
64                    .arg("--version")
65                    .stdout(std::process::Stdio::null())
66                    .stderr(std::process::Stdio::null())
67                    .status()
68                    .is_ok()
69            })
70            .await
71            .unwrap_or(false);
72
73        if found {
74            CheckResult::new(&name, Passed)
75        } else {
76            CheckResult::new(
77                &name,
78                Errored("'nft' binary is required but not present, install `nftables`".into()),
79            )
80        }
81    }
82
83    /// Checks that the given util-linux/coreutils/etc binaries are present in PATH.
84    /// It is assumed that all of these respond to `--version`.
85    /// On all supported systems this should be a given.
86    /// Currently, the installer relies explicitly on these being present on the host.
87    pub async fn has_linux_util_bins(&self, bins: &[&str]) -> CheckResult {
88        let name = String::from("Basic Linux Utility Binaries Present");
89        let owned_bins: Vec<String> = bins.iter().map(|s| s.to_string()).collect();
90
91        let missing = self
92            .host_executor
93            .spawn_in_host_ns(async move {
94                let mut missing = Vec::new();
95                for bin in &owned_bins {
96                    let found = std::process::Command::new(bin)
97                        .arg("--version")
98                        .stdout(std::process::Stdio::null())
99                        .stderr(std::process::Stdio::null())
100                        .status()
101                        .map(|s| s.success())
102                        .unwrap_or(false);
103                    if !found {
104                        missing.push(bin.clone());
105                    }
106                }
107                missing
108            })
109            .await
110            .unwrap_or_default();
111
112        if missing.is_empty() {
113            CheckResult::new(&name, Passed)
114        } else {
115            let msg = format!(
116                "required basic Linux utility binaries not found in PATH: {}",
117                missing.join(", ")
118            );
119            CheckResult::new(&name, Errored(msg))
120        }
121    }
122
123    /// Checks that either `grub-mkconfig` or `grub2-mkconfig` is present in PATH,
124    /// as the installer currently requires one or the other.
125    pub async fn has_grub_mkconfig_bin(&self) -> CheckResult {
126        let name = String::from("'grub-mkconfig' Binary Present");
127        let found = self
128            .host_executor
129            .spawn_in_host_ns(async {
130                ["grub-mkconfig", "grub2-mkconfig"].iter().any(|bin| {
131                    std::process::Command::new(bin)
132                        .arg("--version")
133                        .stdout(std::process::Stdio::null())
134                        .stderr(std::process::Stdio::null())
135                        .status()
136                        .map(|s| s.success())
137                        .unwrap_or(false)
138                })
139            })
140            .await
141            .unwrap_or(false);
142
143        if found {
144            CheckResult::new(&name, Passed)
145        } else {
146            CheckResult::new(
147                &name,
148                Errored("neither 'grub-mkconfig' nor 'grub2-mkconfig' found in PATH".into()),
149            )
150        }
151    }
152
153    /// Checks that either `systemctl` or `rc-update` is present in PATH,
154    /// as the installer requires one or the other to enable services.
155    pub async fn has_service_manager_bin(&self) -> CheckResult {
156        let name = String::from("Service Manager Binary Present");
157        let found = self
158            .host_executor
159            .spawn_in_host_ns(async {
160                ["systemctl", "rc-update"].iter().any(|bin| {
161                    std::process::Command::new(bin)
162                        .arg("--version")
163                        .stdout(std::process::Stdio::null())
164                        .stderr(std::process::Stdio::null())
165                        .status()
166                        .map(|s| s.success())
167                        .unwrap_or(false)
168                })
169            })
170            .await
171            .unwrap_or(false);
172
173        if found {
174            CheckResult::new(&name, Passed)
175        } else {
176            CheckResult::new(
177                &name,
178                Errored("neither 'systemctl' nor 'rc-update' found in PATH".into()),
179            )
180        }
181    }
182
183    /// Checks that at least one supported package manager is present in PATH,
184    /// as the installer requires one to install system packages.
185    pub async fn has_package_manager_bin(&self) -> CheckResult {
186        let name = String::from("Package Manager Binary Present");
187        let found = self
188            .host_executor
189            .spawn_in_host_ns(async {
190                ["dnf", "yum", "zypper", "apt-get", "apk"]
191                    .iter()
192                    .any(|bin| {
193                        std::process::Command::new(bin)
194                            .arg("--version")
195                            .stdout(std::process::Stdio::null())
196                            .stderr(std::process::Stdio::null())
197                            .status()
198                            .map(|s| s.success())
199                            .unwrap_or(false)
200                    })
201            })
202            .await
203            .unwrap_or(false);
204
205        if found {
206            CheckResult::new(&name, Passed)
207        } else {
208            CheckResult::new(
209                &name,
210                Errored("no supported package manager found in PATH (tried: dnf, yum, zypper, apt-get, apk)".into()),
211            )
212        }
213    }
214
215    /// Checks that total system RAM is at least 4 GB.
216    ///
217    /// Manual equivalent:
218    /// ```sh
219    /// awk '/MemTotal/ { if ($2 >= 4194304) print "OK"; else print "FAIL" }' /proc/meminfo
220    /// ```
221    pub async fn enough_memory(&self) -> CheckResult {
222        let name = String::from("Enough Memory");
223
224        let total_mem = match self
225            .host_executor
226            .spawn_in_host_ns(async {
227                let mut sys = System::new_all();
228                sys.refresh_all();
229
230                sys.total_memory()
231            })
232            .await
233        {
234            Ok(mem) => mem,
235            Err(e) => {
236                return CheckResult::new(&name, Errored(e.to_string()));
237            }
238        };
239
240        debug!("total memory = {total_mem}");
241
242        let mut result = Passed;
243        if total_mem < MINIMUM_MEMORY {
244            let reason = format!("total memory is less than {}", MINIMUM_MEMORY);
245            result = Failed(reason);
246        }
247        CheckResult::new(&name, result)
248    }
249
250    /// Checks that at least one mounted filesystem has 20 GB or more of available space.
251    ///
252    /// Manual equivalent:
253    /// ```sh
254    /// df -BG | awk 'NR>1 { gsub(/G/,""); if (int($4) >= 20) found=1 } END { exit !found }'
255    /// ```
256    pub async fn enough_disk(&self) -> CheckResult {
257        let name = String::from("Enough Disk");
258
259        let result = match self
260            .host_executor
261            .spawn_in_host_ns(async {
262                let mut result = Failed(String::from("Not enough disk space on any disk"));
263                let disks = Disks::new_with_refreshed_list();
264                for disk in &disks {
265                    if disk.available_space() < MINIMUM_DISK {
266                        debug!(
267                            "Not enough space on disk mounted at {} - {}",
268                            disk.mount_point().display(),
269                            disk.available_space()
270                        );
271                    } else {
272                        debug!(
273                            "Enough space on disk mounted at {} - {}",
274                            disk.mount_point().display(),
275                            disk.available_space()
276                        );
277                        result = Passed;
278                    }
279                }
280                result
281            })
282            .await
283        {
284            Ok(result) => result,
285            Err(e) => {
286                return CheckResult::new(&name, Errored(e.to_string()));
287            }
288        };
289
290        CheckResult::new(&name, result)
291    }
292}
293
294#[async_trait]
295impl CheckGroup for SystemChecks {
296    fn id(&self) -> &str {
297        GROUP_IDENTIFIER
298    }
299
300    fn name(&self) -> &str {
301        NAME
302    }
303
304    fn description(&self) -> &str {
305        "System requirement checks"
306    }
307
308    async fn run(&self) -> CheckGroupResult {
309        self.run_all().await
310    }
311
312    fn category(&self) -> CheckGroupCategory {
313        CheckGroupCategory::Required
314    }
315}