Skip to main content

retch_sysinfo/
zfs.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! ZFS storage pool detection via `zpool list`.
5//!
6//! Supported wherever the `zpool` binary is present (Linux with ZFS-on-Linux,
7//! macOS with OpenZFS). Returns an empty list if `zpool` is not installed —
8//! most systems don't have ZFS, so a missing binary is not an error.
9
10/// Detects imported ZFS pools and reports name, allocation, and health for each.
11///
12/// Shells out to `zpool list`, so this is gated behind `--long` and above
13/// like other subprocess-based fields (bios, wifi, ...).
14pub fn detect_zpool() -> Vec<String> {
15    let output = std::process::Command::new("zpool")
16        .args(["list", "-H", "-p", "-o", "name,size,alloc,health"])
17        .output();
18
19    let Ok(output) = output else {
20        return Vec::new();
21    };
22    if !output.status.success() {
23        return Vec::new();
24    }
25
26    parse_zpool_list(&String::from_utf8_lossy(&output.stdout))
27}
28
29/// Parses `zpool list -H -p -o name,size,alloc,health` (tab-separated, byte-exact sizes).
30pub fn parse_zpool_list(text: &str) -> Vec<String> {
31    let mut pools = Vec::new();
32
33    for line in text.lines() {
34        let fields: Vec<&str> = line.split('\t').collect();
35        if fields.len() < 4 {
36            continue;
37        }
38        let name = fields[0];
39        let Ok(size) = fields[1].parse::<u64>() else {
40            continue;
41        };
42        let Ok(alloc) = fields[2].parse::<u64>() else {
43            continue;
44        };
45        let health = fields[3];
46
47        let size_gb = size as f64 / 1_073_741_824.0;
48        let alloc_gb = alloc as f64 / 1_073_741_824.0;
49
50        pools.push(format!(
51            "{}: {:.1} / {:.1} GB ({})",
52            name, alloc_gb, size_gb, health
53        ));
54    }
55
56    pools
57}
58
59#[cfg(test)]
60mod tests {
61    use super::parse_zpool_list;
62
63    #[test]
64    fn test_parse_zpool_list_single() {
65        let text = "tank\t1073741824000\t536870912000\tONLINE\n";
66        let pools = parse_zpool_list(text);
67        assert_eq!(pools, vec!["tank: 500.0 / 1000.0 GB (ONLINE)"]);
68    }
69
70    #[test]
71    fn test_parse_zpool_list_multi() {
72        let text = "tank\t1073741824000\t536870912000\tONLINE\nbackup\t2147483648000\t107374182400\tDEGRADED\n";
73        let pools = parse_zpool_list(text);
74        assert_eq!(pools.len(), 2);
75        assert_eq!(pools[0], "tank: 500.0 / 1000.0 GB (ONLINE)");
76        assert_eq!(pools[1], "backup: 100.0 / 2000.0 GB (DEGRADED)");
77    }
78
79    #[test]
80    fn test_parse_zpool_list_empty() {
81        assert!(parse_zpool_list("").is_empty());
82    }
83
84    #[test]
85    fn test_parse_zpool_list_malformed_line_skipped() {
86        let text = "tank\tnot-a-number\t536870912000\tONLINE\n";
87        assert!(parse_zpool_list(text).is_empty());
88    }
89}