#[cfg(target_os = "linux")]
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VdevClass {
#[default]
Data,
Special,
Dedup,
Log,
Cache,
Spare,
}
impl VdevClass {
pub fn label(self) -> &'static str {
match self {
VdevClass::Data => "data",
VdevClass::Special => "special",
VdevClass::Dedup => "dedup",
VdevClass::Log => "log",
VdevClass::Cache => "cache",
VdevClass::Spare => "spare",
}
}
fn from_section(name: &str) -> Option<VdevClass> {
match name {
"special" => Some(VdevClass::Special),
"dedup" => Some(VdevClass::Dedup),
"logs" => Some(VdevClass::Log),
"cache" => Some(VdevClass::Cache),
"spare" => Some(VdevClass::Spare),
_ => None,
}
}
fn holds_pool_data(self) -> bool {
!matches!(self, VdevClass::Cache | VdevClass::Spare)
}
}
#[derive(Debug, Clone, Default)]
pub struct ZfsPool {
pub name: String,
pub size_bytes: u64,
pub alloc_bytes: u64,
pub free_bytes: u64,
pub health: String,
pub vdevs: Vec<ZfsVdev>,
}
#[derive(Debug, Clone, Default)]
pub struct ZfsVdev {
pub name: String,
pub class: VdevClass,
pub size_bytes: u64,
pub alloc_bytes: Option<u64>,
pub health: String,
pub members: Vec<ZfsMember>,
}
#[derive(Debug, Clone, Default)]
pub struct ZfsMember {
pub path: String,
pub health: String,
pub slot: usize,
pub dev: Option<String>,
}
impl ZfsMember {
pub fn display(&self) -> &str {
match &self.dev {
Some(d) => d,
None => self.path.rsplit('/').next().unwrap_or(&self.path),
}
}
}
impl ZfsVdev {
pub fn layout(&self) -> &str {
if !is_group(&self.name) {
return "disk";
}
let base = self.name.split(':').next().unwrap_or(&self.name);
base.rsplit_once('-').map_or(base, |(kind, _)| kind)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn member_shares(&self) -> Vec<(&ZfsMember, u64)> {
let Some(alloc) = self.alloc_bytes else {
return Vec::new();
};
if !self.class.holds_pool_data() || self.members.is_empty() {
return Vec::new();
}
let layout = self.layout();
let share = if layout.starts_with("raidz") || layout.starts_with("draid") {
let slots = self.members.iter().map(|m| m.slot).max().unwrap_or(0) + 1;
alloc / slots as u64
} else {
alloc
};
self.members.iter().map(|m| (m, share)).collect()
}
}
fn is_group(name: &str) -> bool {
(name.starts_with("draid") && name.contains(':'))
|| ["mirror-", "raidz", "replacing-", "spare-"]
.iter()
.any(|p| name.starts_with(p))
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) fn parse_zpool_list(text: &str) -> Vec<ZfsPool> {
fn bytes(s: &str) -> Option<u64> {
s.parse().ok()
}
let mut pools: Vec<ZfsPool> = Vec::new();
let mut class = VdevClass::Data;
let mut slots = 0usize;
let mut skipping = false;
for line in text.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 5 {
continue;
}
let name = fields[0];
let health = fields[fields.len() - 1].to_string();
let depth = line.len() - line.trim_start_matches(' ').len();
if depth == 0 {
if name == "NAME" && fields[1] == "SIZE" {
continue;
}
if fields[1..].iter().all(|f| *f == "-") {
class = VdevClass::from_section(name).unwrap_or(VdevClass::Data);
continue;
}
pools.push(ZfsPool {
name: name.to_string(),
size_bytes: bytes(fields[1]).unwrap_or(0),
alloc_bytes: bytes(fields[2]).unwrap_or(0),
free_bytes: bytes(fields[3]).unwrap_or(0),
health,
vdevs: Vec::new(),
});
class = VdevClass::Data;
continue;
}
let Some(pool) = pools.last_mut() else {
continue;
};
if depth == 2 {
skipping = name.starts_with("indirect-");
if skipping {
continue;
}
slots = 0;
let members = if is_group(name) {
Vec::new()
} else {
vec![ZfsMember {
path: name.to_string(),
health: health.clone(),
slot: 0,
dev: None,
}]
};
pool.vdevs.push(ZfsVdev {
name: name.to_string(),
class,
size_bytes: bytes(fields[1]).unwrap_or(0),
alloc_bytes: bytes(fields[2]),
health,
members,
});
continue;
}
if skipping {
continue;
}
let Some(vdev) = pool.vdevs.last_mut() else {
continue;
};
let slot = if depth == 4 {
slots += 1;
slots - 1
} else {
slots.saturating_sub(1)
};
if is_group(name) {
continue;
}
vdev.members.push(ZfsMember {
path: name.to_string(),
health,
slot,
dev: None,
});
}
pools
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Default)]
pub struct ZfsProbe {
pub pools: Vec<ZfsPool>,
pub note: Option<String>,
}
#[cfg(target_os = "linux")]
pub fn probe() -> ZfsProbe {
use std::sync::Mutex;
const FRESH: Duration = Duration::from_secs(5);
const RETRY_AFTER_TIMEOUT: Duration = Duration::from_secs(300);
struct Cached {
at: Instant,
timed_out: bool,
probe: ZfsProbe,
}
static CACHE: Mutex<Option<Cached>> = Mutex::new(None);
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = cache.as_ref() {
let ttl = if c.timed_out {
RETRY_AFTER_TIMEOUT
} else {
FRESH
};
if c.at.elapsed() < ttl {
return c.probe.clone();
}
}
let outcome = run_zpool();
let timed_out = matches!(outcome, Run::TimedOut);
let last = cache.take().map(|c| c.probe.pools).unwrap_or_default();
let zfs_loaded = std::path::Path::new("/sys/module/zfs").exists();
let probe = settle(outcome, last, zfs_loaded);
*cache = Some(Cached {
at: Instant::now(),
timed_out,
probe: probe.clone(),
});
probe
}
#[cfg(target_os = "linux")]
const ZPOOL_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(target_os = "linux")]
fn run_zpool() -> Run {
const ZPOOL: [&str; 3] = ["zpool", "/usr/sbin/zpool", "/sbin/zpool"];
const ARGS: [&str; 6] = [
"list",
"-v",
"-P",
"-p",
"-o",
"name,size,allocated,free,health",
];
let mut outcome = Run::Missing;
for program in ZPOOL {
outcome = run_with_timeout(program, &ARGS, ZPOOL_TIMEOUT);
if !matches!(outcome, Run::Missing) {
break;
}
}
outcome
}
#[cfg(target_os = "linux")]
fn settle(outcome: Run, last: Vec<ZfsPool>, zfs_loaded: bool) -> ZfsProbe {
let text = match outcome {
Run::Ok(text) => text,
failed => {
let kept = !last.is_empty();
return ZfsProbe {
note: (kept || zfs_loaded).then(|| failure_note(&failed, kept)),
pools: last,
};
}
};
let mut pools = parse_zpool_list(&text);
for m in pools
.iter_mut()
.flat_map(|p| p.vdevs.iter_mut())
.flat_map(|v| v.members.iter_mut())
{
m.dev = kernel_name(&m.path);
}
ZfsProbe { pools, note: None }
}
#[cfg(target_os = "linux")]
fn kernel_name(path: &str) -> Option<String> {
if !path.starts_with("/dev/") {
return None;
}
let real = std::fs::canonicalize(path).ok()?;
let name = real.to_str()?.strip_prefix("/dev/")?;
(!name.is_empty() && !name.contains('/')).then(|| name.to_string())
}
#[cfg(target_os = "linux")]
enum Run {
Ok(String),
Missing,
TimedOut,
Failed(Option<i32>),
}
#[cfg(target_os = "linux")]
fn failure_note(run: &Run, kept: bool) -> String {
let why = match run {
Run::Ok(_) => return String::new(),
Run::Missing => "ZFS is loaded but zpool was not found".to_string(),
Run::TimedOut => format!(
"zpool list did not answer within {}s",
ZPOOL_TIMEOUT.as_secs()
),
Run::Failed(Some(code)) => format!("zpool list failed (exit {code})"),
Run::Failed(None) => "zpool list failed".to_string(),
};
let shown = if kept {
"showing pools as last listed"
} else {
"pools not shown"
};
format!("{why}; {shown}")
}
#[cfg(target_os = "linux")]
fn run_with_timeout(program: &str, args: &[&str], timeout: Duration) -> Run {
use std::io::Read;
use std::process::{Command, Stdio};
let mut child = match Command::new(program)
.args(args)
.env("LC_ALL", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Run::Missing,
Err(_) => return Run::Failed(None),
};
let Some(mut stdout) = child.stdout.take() else {
return Run::Failed(None);
};
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut buf = Vec::new();
let _ = stdout.read_to_end(&mut buf);
let _ = tx.send(buf);
});
let abandon = |mut child: std::process::Child| {
let _ = child.kill();
std::thread::spawn(move || child.wait());
Run::TimedOut
};
let deadline = Instant::now() + timeout;
let Ok(buf) = rx.recv_timeout(timeout) else {
return abandon(child);
};
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => {
return Run::Ok(String::from_utf8_lossy(&buf).into_owned());
}
Ok(Some(status)) => return Run::Failed(status.code()),
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(5));
}
Ok(None) => return abandon(child),
Err(_) => return Run::Failed(None),
}
}
}
#[cfg(test)]
pub(crate) mod fixtures {
pub const SINGLE: &str = "\
NAME SIZE ALLOC FREE HEALTH
tank 994558394368 412334399488 582223994880 ONLINE
/dev/disk/by-id/ata-Samsung_SSD_870_EVO_1TB_S6PTNZ0R612345X-part3 995635478528 412334399488 582223994880 - - 4 41 - ONLINE
";
pub const MIRROR: &str = "\
NAME SIZE ALLOC FREE HEALTH
backup 3985729650688 2103012589568 1882717061120 ONLINE
mirror-0 3985729650688 2103012589568 1882717061120 - - 11 52 - ONLINE
/dev/disk/by-id/ata-WDC_WD40EFRX-68N32N0_WD-WCC7K4XYZ123-part1 4000776716288 - - - - - - - ONLINE
/dev/disk/by-id/ata-WDC_WD40EFRX-68N32N0_WD-WCC7K7ABC456-part1 4000776716288 - - - - - - - ONLINE
";
pub const RAIDZ1: &str = "\
NAME SIZE ALLOC FREE HEALTH
media 11995970224128 6598134841344 5397835382784 ONLINE
raidz1-0 11995970224128 6598134841344 5397835382784 - - 2 55 - ONLINE
/dev/sdb1 4000776716288 - - - - - - - ONLINE
/dev/sdc1 4000776716288 - - - - - - - ONLINE
/dev/sdd1 4000776716288 - - - - - - - ONLINE
";
pub const CLASSES: &str = "\
NAME SIZE ALLOC FREE HEALTH
tank 4485716738048 1250157830144 3235558907904 ONLINE
mirror-0 3985729650688 1201865441280 2783864209408 - - 7 30 - ONLINE
/dev/sda1 4000776716288 - - - - - - - ONLINE
/dev/sdb1 4000776716288 - - - - - - - ONLINE
special - - - - - - - - -
mirror-1 499987087360 48292388864 451694698496 - - 3 9 - ONLINE
/dev/nvme0n1p1 500105217024 - - - - - - - ONLINE
/dev/nvme1n1p1 500105217024 - - - - - - - ONLINE
logs - - - - - - - - -
/dev/nvme2n1p1 17179869184 1310720 16641687552 - - 0 0 - ONLINE
cache - - - - - - - - -
/dev/nvme2n1p2 233001975808 101468766208 131441754112 - - 0 43 - ONLINE
spare - - - - - - - - -
/dev/sdc1 4000776716288 - - - - - - - AVAIL
";
pub const CLASSES_V24: &str = "\
NAME SIZE ALLOC FREE HEALTH
tank 4485716738048 1250157830144 3235558907904 ONLINE
mirror-0 3985729650688 1201865441280 2783864209408 ONLINE
/dev/sda1 4000776716288 - - ONLINE
/dev/sdb1 4000776716288 - - ONLINE
special - - - -
mirror-1 499987087360 48292388864 451694698496 ONLINE
/dev/nvme0n1p1 500105217024 - - ONLINE
/dev/nvme1n1p1 500105217024 - - ONLINE
logs - - - -
/dev/nvme2n1p1 17179869184 1310720 16641687552 ONLINE
cache - - - -
/dev/nvme2n1p2 233001975808 101468766208 131441754112 ONLINE
spare - - - -
/dev/sdc1 4000776716288 - - AVAIL
";
pub const UBUNTU: &str = "\
NAME SIZE ALLOC FREE HEALTH
bpool 4160749568 412090368 3748659200 ONLINE
mirror-0 4160749568 412090368 3748659200 - - 0 9 - ONLINE
/dev/disk/by-id/nvme-Sabrent_SB-RKT4P-1TB_48790469800117-part2 4294967296 - - - - - - - ONLINE
/dev/disk/by-id/nvme-Lexar_SSD_NM790_1TB_QLD587H007580P2202-part2 4294967296 - - - - - - - ONLINE
rpool 991111553024 58128883712 932982669312 ONLINE
mirror-0 991111553024 58128883712 932982669312 - - 3 5 - ONLINE
/dev/disk/by-id/nvme-Sabrent_SB-RKT4P-1TB_48790469800117-part3 994835120128 - - - - - - - ONLINE
/dev/disk/by-id/nvme-Lexar_SSD_NM790_1TB_QLD587H007580P2202-part3 994835120128 - - - - - - - ONLINE
";
pub const UBUNTU_V24: &str = "\
NAME SIZE ALLOC FREE HEALTH
bpool 4160749568 412090368 3748659200 ONLINE
mirror-0 4160749568 412090368 3748659200 ONLINE
/dev/disk/by-id/nvme-Sabrent_SB-RKT4P-1TB_48790469800117-part2 4294967296 - - ONLINE
/dev/disk/by-id/nvme-Lexar_SSD_NM790_1TB_QLD587H007580P2202-part2 4294967296 - - ONLINE
rpool 991111553024 58128883712 932982669312 ONLINE
mirror-0 991111553024 58128883712 932982669312 ONLINE
/dev/disk/by-id/nvme-Sabrent_SB-RKT4P-1TB_48790469800117-part3 994835120128 - - ONLINE
/dev/disk/by-id/nvme-Lexar_SSD_NM790_1TB_QLD587H007580P2202-part3 994835120128 - - ONLINE
";
pub const DEGRADED: &str = "\
NAME SIZE ALLOC FREE HEALTH
rpool 991111553024 58128883712 932982669312 DEGRADED
mirror-0 991111553024 58128883712 932982669312 - - 3 5 - DEGRADED
/dev/disk/by-id/nvme-Sabrent_SB-RKT4P-1TB_48790469800117-part3 994835120128 - - - - - - - OFFLINE
/dev/disk/by-id/nvme-Lexar_SSD_NM790_1TB_QLD587H007580P2202-part3 994835120128 - - - - - - - ONLINE
media 11995970224128 6598134841344 5397835382784 DEGRADED
raidz1-0 11995970224128 6598134841344 5397835382784 - - 2 55 - DEGRADED
/dev/sdb1 4000776716288 - - - - - - - ONLINE
2817390427383737393 - - - - - - - - UNAVAIL
spare-2 - - - - - - - - DEGRADED
/dev/sdd1 4000776716288 - - - - - - - FAULTED
/dev/sde1 4000776716288 - - - - - - - ONLINE
spare - - - - - - - - -
/dev/sde1 4000776716288 - - - - - - - INUSE
";
pub fn resolve(path: &str) -> Option<String> {
if let Some(part) = path
.rsplit("-part")
.next()
.filter(|_| path.contains("-part"))
{
if path.contains("Sabrent") {
return Some(format!("nvme0n1p{part}"));
}
if path.contains("Lexar") {
return Some(format!("nvme1n1p{part}"));
}
if path.contains("Samsung") {
return Some(format!("sda{part}"));
}
if path.contains("WCC7K4XYZ123") {
return Some(format!("sdb{part}"));
}
if path.contains("WCC7K7ABC456") {
return Some(format!("sdc{part}"));
}
}
path.strip_prefix("/dev/").map(str::to_string)
}
pub fn pools(text: &str) -> Vec<super::ZfsPool> {
let mut pools = super::parse_zpool_list(text);
for m in pools
.iter_mut()
.flat_map(|p| p.vdevs.iter_mut())
.flat_map(|v| v.members.iter_mut())
{
m.dev = resolve(&m.path);
}
pools
}
}
#[cfg(test)]
mod tests {
use super::fixtures::*;
use super::*;
fn shares(v: &ZfsVdev) -> Vec<(String, u64)> {
v.member_shares()
.into_iter()
.map(|(m, b)| (m.display().to_string(), b))
.collect()
}
#[test]
fn single_disk_pool_on_a_by_id_partition() {
let pools = pools(SINGLE);
assert_eq!(pools.len(), 1);
let p = &pools[0];
assert_eq!(p.name, "tank");
assert_eq!(p.size_bytes, 994_558_394_368);
assert_eq!(p.alloc_bytes, 412_334_399_488);
assert_eq!(p.free_bytes, 582_223_994_880);
assert_eq!(p.health, "ONLINE");
assert_eq!(p.vdevs.len(), 1);
let v = &p.vdevs[0];
assert_eq!(v.layout(), "disk");
assert_eq!(v.class, VdevClass::Data);
assert_eq!(v.alloc_bytes, Some(412_334_399_488));
assert_eq!(v.members.len(), 1);
assert_eq!(
v.members[0].path,
"/dev/disk/by-id/ata-Samsung_SSD_870_EVO_1TB_S6PTNZ0R612345X-part3"
);
assert_eq!(v.members[0].display(), "sda3");
assert_eq!(shares(v), vec![("sda3".to_string(), 412_334_399_488)]);
}
#[test]
fn every_mirror_member_holds_the_whole_allocation() {
let pools = pools(MIRROR);
let v = &pools[0].vdevs[0];
assert_eq!(v.name, "mirror-0");
assert_eq!(v.layout(), "mirror");
assert_eq!(v.members.len(), 2);
assert_eq!(
shares(v),
vec![
("sdb1".to_string(), 2_103_012_589_568),
("sdc1".to_string(), 2_103_012_589_568),
]
);
}
#[test]
fn raidz_splits_its_allocation_evenly() {
let pools = pools(RAIDZ1);
let v = &pools[0].vdevs[0];
assert_eq!(v.layout(), "raidz1");
let s = shares(v);
assert_eq!(s.len(), 3);
for (dev, bytes) in &s {
assert_eq!(*bytes, 6_598_134_841_344 / 3, "{dev}");
}
assert_eq!(
s.iter().map(|(d, _)| d.as_str()).collect::<Vec<_>>(),
["sdb1", "sdc1", "sdd1"]
);
}
#[test]
fn allocation_classes_are_told_apart() {
for text in [CLASSES, CLASSES_V24] {
let pools = pools(text);
assert_eq!(pools.len(), 1, "the section rows are not pools");
let classes: Vec<(VdevClass, &str)> = pools[0]
.vdevs
.iter()
.map(|v| (v.class, v.layout()))
.collect();
assert_eq!(
classes,
[
(VdevClass::Data, "mirror"),
(VdevClass::Special, "mirror"),
(VdevClass::Log, "disk"),
(VdevClass::Cache, "disk"),
(VdevClass::Spare, "disk"),
]
);
let by_class = |c: VdevClass| {
let v = pools[0].vdevs.iter().find(|v| v.class == c).unwrap();
shares(v)
};
assert_eq!(
by_class(VdevClass::Special),
vec![
("nvme0n1p1".to_string(), 48_292_388_864),
("nvme1n1p1".to_string(), 48_292_388_864),
]
);
assert_eq!(
by_class(VdevClass::Log),
vec![("nvme2n1p1".to_string(), 1_310_720)]
);
assert!(by_class(VdevClass::Cache).is_empty());
assert!(by_class(VdevClass::Spare).is_empty());
let spare = pools[0].vdevs.last().unwrap();
assert_eq!(spare.health, "AVAIL");
assert_eq!(spare.alloc_bytes, None);
}
}
#[test]
fn ubuntu_bpool_and_rpool_span_both_disks() {
for text in [UBUNTU, UBUNTU_V24] {
let pools = pools(text);
let names: Vec<&str> = pools.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["bpool", "rpool"]);
let rpool = &pools[1];
assert_eq!(rpool.size_bytes, 991_111_553_024);
assert_eq!(rpool.alloc_bytes, 58_128_883_712);
let members: Vec<&str> = rpool.vdevs[0].members.iter().map(|m| m.display()).collect();
assert_eq!(members, ["nvme0n1p3", "nvme1n1p3"]);
let bpool: Vec<&str> = pools[0].vdevs[0]
.members
.iter()
.map(|m| m.display())
.collect();
assert_eq!(bpool, ["nvme0n1p2", "nvme1n1p2"]);
}
}
#[test]
fn degraded_pools_keep_their_shape() {
let pools = pools(DEGRADED);
assert_eq!(pools.len(), 2);
let rpool = &pools[0];
assert_eq!(rpool.health, "DEGRADED");
let mirror = &rpool.vdevs[0];
assert_eq!(mirror.health, "DEGRADED");
let states: Vec<&str> = mirror.members.iter().map(|m| m.health.as_str()).collect();
assert_eq!(states, ["OFFLINE", "ONLINE"]);
assert_eq!(shares(mirror).len(), 2);
assert!(shares(mirror).iter().all(|(_, b)| *b == 58_128_883_712));
let media = &pools[1];
let raidz = &media.vdevs[0];
assert_eq!(raidz.layout(), "raidz1");
let members: Vec<(&str, &str, usize)> = raidz
.members
.iter()
.map(|m| (m.display(), m.health.as_str(), m.slot))
.collect();
assert_eq!(
members,
[
("sdb1", "ONLINE", 0),
("2817390427383737393", "UNAVAIL", 1),
("sdd1", "FAULTED", 2),
("sde1", "ONLINE", 2),
]
);
let third = 6_598_134_841_344 / 3;
assert!(shares(raidz).iter().all(|(_, b)| *b == third));
assert_eq!(raidz.members[1].dev, None);
let spare = media.vdevs.iter().find(|v| v.class == VdevClass::Spare);
assert_eq!(spare.map(|v| v.health.as_str()), Some("INUSE"));
}
#[test]
fn no_pools_parses_to_nothing() {
assert!(parse_zpool_list("no pools available\n").is_empty());
assert!(parse_zpool_list("").is_empty());
}
#[test]
fn a_faulted_pool_is_still_a_pool() {
let text = "\
NAME SIZE ALLOC FREE HEALTH
tank - - - FAULTED
mirror-0 - - - - - - - - UNAVAIL
/dev/sdb1 - - - - - - - - UNAVAIL
";
let pools = parse_zpool_list(text);
assert_eq!(pools.len(), 1);
assert_eq!(pools[0].health, "FAULTED");
assert_eq!(pools[0].size_bytes, 0);
assert!(pools[0].vdevs[0].member_shares().is_empty());
}
#[test]
fn removed_vdevs_are_skipped() {
let text = "\
NAME SIZE ALLOC FREE HEALTH
tank 1000 400 600 ONLINE
/dev/sdb1 1100 400 600 - - 1 40 - ONLINE
indirect-1 - - - - - - - - ONLINE
";
let pools = parse_zpool_list(text);
assert_eq!(pools[0].vdevs.len(), 1);
assert_eq!(pools[0].vdevs[0].name, "/dev/sdb1");
}
#[test]
fn draid_names_reduce_to_their_layout() {
let v = ZfsVdev {
name: "draid2:4d:12c:1s-0".into(),
..Default::default()
};
assert_eq!(v.layout(), "draid2");
let v = ZfsVdev {
name: "raidz3-1".into(),
..Default::default()
};
assert_eq!(v.layout(), "raidz3");
assert!(!is_group("draid2-0-0"));
}
#[cfg(target_os = "linux")]
#[test]
fn a_missing_binary_is_not_an_error() {
assert!(matches!(
run_with_timeout("/nonexistent/zpool", &["list"], Duration::from_millis(500)),
Run::Missing
));
if !std::path::Path::new("/sys/module/zfs").exists() {
let outcome = run_zpool();
assert!(!matches!(outcome, Run::TimedOut));
let probe = settle(outcome, Vec::new(), false);
if probe.pools.is_empty() {
assert!(probe.note.is_none(), "{:?}", probe.note);
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn a_failed_run_keeps_the_last_pools() {
let last = pools(UBUNTU);
for (failed, why) in [
(Run::TimedOut, "zpool list did not answer within 2s"),
(Run::Failed(Some(1)), "zpool list failed (exit 1)"),
] {
let probe = settle(failed, last.clone(), true);
let names: Vec<&str> = probe.pools.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["bpool", "rpool"]);
assert_eq!(probe.pools[1].vdevs[0].members[0].display(), "nvme0n1p3");
assert_eq!(
probe.note.as_deref(),
Some(format!("{why}; showing pools as last listed").as_str())
);
}
let probe = settle(Run::TimedOut, Vec::new(), true);
assert!(probe.pools.is_empty());
assert_eq!(
probe.note.as_deref(),
Some("zpool list did not answer within 2s; pools not shown")
);
let probe = settle(Run::Ok(MIRROR.to_string()), last.clone(), true);
assert_eq!(probe.pools[0].name, "backup");
assert_eq!(probe.note, None);
let probe = settle(Run::Ok("no pools available\n".to_string()), last, true);
assert!(probe.pools.is_empty());
assert_eq!(probe.note, None);
}
#[cfg(target_os = "linux")]
#[test]
fn a_hung_command_is_abandoned() {
let start = Instant::now();
let out = run_with_timeout("sleep", &["10"], Duration::from_millis(200));
assert!(matches!(out, Run::TimedOut));
assert!(start.elapsed() < Duration::from_secs(2));
assert!(matches!(
run_with_timeout("false", &[], Duration::from_secs(2)),
Run::Failed(Some(1))
));
assert!(matches!(
run_with_timeout("echo", &["ok"], Duration::from_secs(2)),
Run::Ok(s) if s == "ok\n"
));
}
}