use gunnar_proto::{FilesystemUse, GlanceLine};
pub const OK: &str = "ok";
pub const WARN: &str = "warn";
pub const BAD: &str = "bad";
pub const UNKNOWN: &str = "unknown";
pub const WARN_USED_PERCENT: u64 = 80;
pub const BAD_USED_PERCENT: u64 = 90;
#[derive(Debug, Clone, Default)]
pub struct Facts<'a> {
pub record_present: bool,
pub data_label: &'a str,
pub data_shape: &'a str,
pub data_devices: &'a [String],
pub serialdat_label: &'a str,
pub serialdat_class: &'a str,
pub serialdat_device: &'a str,
pub off_stripe: &'a str,
pub off_stripe_health: &'a [String],
pub data_fs: Option<&'a FilesystemUse>,
pub disk: Option<&'a crate::disktest::Outcome>,
}
pub fn human(n: u64) -> String {
const K: f64 = 1024.0;
let v = n as f64;
if v >= K * K * K * K {
format!("{:.1} TiB", v / (K * K * K * K))
} else if v >= K * K * K {
format!("{:.1} GiB", v / (K * K * K))
} else if v >= K * K {
format!("{:.1} MiB", v / (K * K))
} else if v >= K {
format!("{:.1} KiB", v / K)
} else {
format!("{n} B")
}
}
fn line(key: &str, line: String, state: &str, detail: String) -> GlanceLine {
GlanceLine {
key: key.to_owned(),
line,
state: state.to_owned(),
detail,
}
}
pub fn data_line(f: &Facts) -> GlanceLine {
let label = f.data_label;
if !f.record_present {
return line(
"data",
format!("{label}: shape not recorded"),
UNKNOWN,
"This box has no layout config (or it did not verify), so the installer's shape of the \
data set is not known — and it is not guessed from what happens to be mounted."
.into(),
);
}
let devices = f.data_devices.join(", ");
match f.data_shape {
"raid0" if f.data_devices.len() >= 2 => line(
"data",
format!("{label} = raid0 over {devices}"),
OK,
"The repositories are on the data stripe, laid by the installer over the data disks."
.into(),
),
"plain" | "raid0" if !f.data_devices.is_empty() => line(
"data",
format!("{label} = {} on {devices}", f.data_shape),
OK,
"The repositories are on a data disk of their own (one disk, no stripe).".into(),
),
"p3" => line(
"data",
format!("{label} = p3 on the boot disk"),
WARN,
"No data disk was found at install time: the repositories live on the boot disk's \
third partition, beside the system."
.into(),
),
"" => line(
"data",
format!("{label}: the layout config names no shape"),
UNKNOWN,
"The config carries no `data.shape`.".into(),
),
other => line(
"data",
if devices.is_empty() {
format!("{label} = {other}")
} else {
format!("{label} = {other} over {devices}")
},
UNKNOWN,
format!("`data.shape = {other}` is a shape this build has no verdict for."),
),
}
}
pub fn serialdat_line(f: &Facts) -> GlanceLine {
let label = f.serialdat_label;
let dev = f.serialdat_device;
let text = match (f.serialdat_class, dev) {
("p3", "") => format!("{label} on the boot disk (P3)"),
("p3", dev) => format!("{label} on {dev} (boot disk P3)"),
("device", "") => format!("{label} on a disk of its own"),
("device", dev) => format!("{label} on {dev} (its own disk)"),
("fallback", _) => {
format!("{label} is a directory on the data set (no partition of its own)")
}
_ if label.is_empty() => "/serialdat: nothing said where it lives".to_owned(),
_ => format!("{label}: nothing said what disk it is on"),
};
let state = match f.off_stripe {
"off-stripe" => OK,
"same-device" | "absent" => BAD,
_ => UNKNOWN,
};
let verdict = match f.off_stripe {
"off-stripe" => " — the off-stripe copies are on another failure domain",
"same-device" => {
" — the off-stripe copies share a device with the data (no second failure domain)"
}
"absent" => " — there is no off-stripe copy",
_ => "",
};
line(
"serialdat",
format!("{text}{verdict}"),
state,
f.off_stripe_health.join(" "),
)
}
pub fn free_line(f: &Facts) -> GlanceLine {
let label = f.data_label;
match f.data_fs {
Some(fs) if fs.measured && fs.total_bytes > 0 => {
let used = fs.total_bytes.saturating_sub(fs.free_bytes);
let pct = used.saturating_mul(100) / fs.total_bytes;
let state = if pct >= BAD_USED_PERCENT {
BAD
} else if pct >= WARN_USED_PERCENT {
WARN
} else {
OK
};
line(
"free",
format!("{label}: {} free of {} ({pct} % used)", human(fs.free_bytes), human(fs.total_bytes)),
state,
format!(
"Amber from {WARN_USED_PERCENT} % used, red from {BAD_USED_PERCENT} % — the marks the \
estate's growth acts on."
),
)
}
Some(fs) => line(
"free",
format!("{label}: free space not measured"),
UNKNOWN,
if fs.error.is_empty() {
"statvfs answered no size".to_owned()
} else {
fs.error.clone()
},
),
None => line(
"free",
format!("{label}: free space not measured"),
UNKNOWN,
String::new(),
),
}
}
pub fn speed_line(f: &Facts) -> GlanceLine {
use crate::disktest::Outcome;
match f.disk {
Some(Outcome::Done(r)) => line(
"speed",
format!("disk speed: {}", r.row()),
OK,
"Measured once at start-up on the data root: sequential write and read, and 4 KiB \
writes each followed by fsync."
.into(),
),
Some(Outcome::Skipped { free, need }) => line(
"speed",
format!(
"disk speed: not measured — {} free, the test needs {}",
human(*free),
human(*need)
),
WARN,
"The start-up test never fills the disk it measures.".into(),
),
Some(Outcome::Failed(why)) => line(
"speed",
format!("disk speed: the test failed — {why}"),
BAD,
String::new(),
),
None => line(
"speed",
"disk speed: not measured yet".to_owned(),
UNKNOWN,
"The start-up disk test has not finished a run in this process (it runs on the \
appliance, or with GUNNAR_DISKTEST set)."
.into(),
),
}
}
pub fn lines(f: &Facts) -> Vec<GlanceLine> {
vec![data_line(f), serialdat_line(f), free_line(f), speed_line(f)]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::disktest::{Outcome, ReadHow, Report};
use std::time::Duration;
fn devs(d: &[&str]) -> Vec<String> {
d.iter().map(|s| s.to_string()).collect()
}
fn estate<'a>(devices: &'a [String], fs: &'a FilesystemUse) -> Facts<'a> {
Facts {
record_present: true,
data_label: "/data",
data_shape: "raid0",
data_devices: devices,
serialdat_label: "/serialdat",
serialdat_class: "p3",
serialdat_device: "/dev/vda3",
off_stripe: "off-stripe",
off_stripe_health: &[],
data_fs: Some(fs),
disk: None,
}
}
fn fs(total: u64, free: u64) -> FilesystemUse {
FilesystemUse {
what: "data root".into(),
path: "/data".into(),
measured: true,
total_bytes: total,
free_bytes: free,
error: String::new(),
}
}
#[test]
fn data_names_its_own_devices_and_serialdat_names_the_boot_disk() {
let d = devs(&["/dev/vdb", "/dev/vdc"]);
let fs = fs(100 << 30, 60 << 30);
let f = estate(&d, &fs);
let data = data_line(&f);
assert_eq!(
(data.line.as_str(), data.state.as_str()),
("/data = raid0 over /dev/vdb, /dev/vdc", "ok")
);
assert!(
!data.line.contains("vda3"),
"the data line must not name the boot disk: {}",
data.line
);
let sd = serialdat_line(&f);
assert!(
sd.line
.starts_with("/serialdat on /dev/vda3 (boot disk P3)"),
"{}",
sd.line
);
assert_eq!(sd.state, "ok");
let keys: Vec<_> = lines(&f).into_iter().map(|l| l.key).collect();
assert_eq!(keys, ["data", "serialdat", "free", "speed"]);
}
#[test]
fn the_off_stripe_copies_are_green_only_when_off_stripe() {
let d = devs(&["/dev/vdb"]);
let fs = fs(1, 1);
for (word, state) in [
("off-stripe", "ok"),
("same-device", "bad"),
("absent", "bad"),
("", "unknown"),
("weird", "unknown"),
] {
let mut f = estate(&d, &fs);
f.off_stripe = word;
assert_eq!(serialdat_line(&f).state, state, "{word}");
}
}
#[test]
fn the_data_shapes_each_have_their_words() {
let one = devs(&["/dev/vdb"]);
let none: Vec<String> = Vec::new();
let fs = fs(1, 1);
let mut f = estate(&one, &fs);
f.data_shape = "plain";
assert_eq!(
(data_line(&f).line.as_str(), data_line(&f).state.as_str()),
("/data = plain on /dev/vdb", "ok")
);
let mut f = estate(&none, &fs);
f.data_shape = "p3";
assert_eq!(data_line(&f).state, "warn");
f.record_present = false;
assert_eq!(data_line(&f).state, "unknown", "no record: not guessed");
}
#[test]
fn free_space_turns_amber_then_red_and_unmeasured_is_unknown() {
let d = devs(&["/dev/vdb", "/dev/vdc"]);
for (free, state) in [(50u64, "ok"), (19, "warn"), (5, "bad")] {
let fs = fs(100, free);
assert_eq!(
free_line(&estate(&d, &fs)).state,
state,
"{free} free of 100"
);
}
let broken = FilesystemUse {
measured: false,
error: "EIO".into(),
..fs(0, 0)
};
let l = free_line(&estate(&d, &broken));
assert_eq!(l.state, "unknown");
assert!(
!l.line.chars().any(|c| c.is_ascii_digit()),
"no number that was not read: {}",
l.line
);
}
#[test]
fn the_disk_speed_is_green_only_when_measured() {
let d = devs(&["/dev/vdb", "/dev/vdc"]);
let fs = fs(1, 1);
let mut f = estate(&d, &fs);
assert_eq!(speed_line(&f).state, "unknown");
let done = Outcome::Done(Report {
seq_write_bps: 8.12e8,
seq_read_bps: 1.0e9,
read_how: ReadHow::Direct,
sync_ops_per_s: 3100.0,
elapsed: Duration::from_millis(1600),
file_bytes: 1 << 30,
write_elapsed: Duration::from_millis(800),
read_elapsed: Duration::from_millis(500),
sync_ops: 4960,
sync_elapsed: Duration::from_millis(300),
});
f.disk = Some(&done);
let l = speed_line(&f);
assert_eq!(l.state, "ok");
assert!(l.line.contains("812 MB/s"), "{}", l.line);
let failed = Outcome::Failed("EIO".into());
f.disk = Some(&failed);
assert_eq!(speed_line(&f).state, "bad");
}
}