server/lib/src/admin_view/disks.rs
//! **The Disks pane, the disk speed test and the twin's credential read** (`estate`):
//! edda `disk-snapshot` and nordisk-twin's certificate reading, for the admin view.
use super::*;
/// Whether this process started the disk speed test (and over which root).
static SPEED_ROOT: OnceLock<PathBuf> = OnceLock::new();
/// An installed credential onto the pane: its doors, principal, issuer and end.
pub(crate) fn installed(
c: &mut pb::AdminTwinCredential,
cred: &crate::twin_tls::Credential,
now_unix: i64,
) {
c.installed = true;
c.doors = cred.doors();
c.principal = nordisk_twin::pki::common_name_of_cert(&cred.cert_pem)
.ok()
.flatten()
.unwrap_or_default();
c.issuer = nordisk_twin::pki::common_name_of_cert(&cred.ca_pem)
.ok()
.flatten()
.unwrap_or_default();
match nordisk_twin::pki::validity(&cred.cert_pem) {
Ok((_, not_after)) => {
c.not_after_unix = not_after;
c.days_left = (not_after - now_unix).div_euclid(86_400);
}
Err(e) => c.error = format!("the installed certificate's validity cannot be read: {e}"),
}
}
/// The floor the Disks pane computes with — advisory: holger refuses no write at it.
pub const FLOOR: disk_snapshot::Floor = disk_snapshot::Floor::DEFAULT;
/// [`FLOOR`] in words, on every answer, so nobody reads it as enforced.
pub const FLOOR_RULE: &str = "advisory: 5 % of the filesystem or 2 GiB, whichever is larger (edda disk-snapshot Floor::DEFAULT, gunnar's --disk-floor rule) — holger refuses no write at it today";
// ── Disks ───────────────────────────────────────────────────────────────────
/// **The paths holger cares about**, labelled: `system` = `/`, `data` = the data
/// root, `storage:<repo>` = every place a repository keeps its bytes (a path
/// named twice under one label is listed once).
pub fn volumes(
data_root: Option<&Path>,
places: &crate::metering::StoragePlaces,
) -> Vec<disk_snapshot::Volume> {
let mut v = vec![disk_snapshot::Volume::new("system", "/")];
if let Some(root) = data_root {
v.push(disk_snapshot::Volume::new("data", root));
}
for (repo, ps) in places {
let mut seen: Vec<&PathBuf> = Vec::new();
for p in ps {
if seen.contains(&&p.path) {
continue;
}
seen.push(&p.path);
v.push(disk_snapshot::Volume::new(
format!("storage:{repo}"),
&p.path,
));
}
}
v
}
/// What the Disks pane says of a repository's storage folder that does not exist yet.
pub const MADE_AT_FIRST_UPLOAD: &str =
"created at the first upload: holger makes this folder when it first stores something here";
/// The Disks pane over this machine (blocking: statvfs + sysfs).
pub fn disks(volumes: &[disk_snapshot::Volume]) -> pb::AdminDisks {
disks_from(disk_snapshot::take(volumes, FLOOR))
}
/// A snapshot onto the wire, asking the filesystem whether each unplaced path exists.
pub fn disks_from(s: disk_snapshot::Snapshot) -> pb::AdminDisks {
disks_from_with(s, &|p: &Path| p.exists())
}
/// Pure: a snapshot onto the wire; `exists` says whether a path is there.
pub fn disks_from_with(
s: disk_snapshot::Snapshot,
exists: &dyn Fn(&Path) -> bool,
) -> pb::AdminDisks {
pb::AdminDisks {
mounts: s
.disks
.into_iter()
.map(|d| {
let (f, error) = match d.figures {
Ok(f) => (f, String::new()),
Err(why) => (ZERO, why),
};
pb::AdminDisk {
source: d.mount.source,
mount_point: d.mount.mount_point.display().to_string(),
fstype: d.mount.fstype,
read_only: d.mount.read_only,
labels: d.labels,
paths: d.paths.iter().map(|p| p.display().to_string()).collect(),
total_bytes: f.total_bytes,
free_bytes: f.free_bytes,
available_bytes: f.available_bytes,
used_bytes: f.used_bytes,
reserved_bytes: f.reserved_bytes,
inodes_total: f.inodes_total,
inodes_free: f.inodes_free,
block_size: if error.is_empty() { f.block_size } else { 0 },
floor_bytes: d.floor_bytes,
servable_bytes: d.servable_bytes,
used_permille: f.used_permille(),
error,
}
})
.collect(),
block_devices: s
.block_devices
.into_iter()
.map(|b| pb::AdminBlockDevice {
name: b.name,
size_bytes: b.size_bytes,
rotational: b.rotational,
removable: b.removable,
read_only: b.read_only,
partitions: b
.partitions
.into_iter()
.map(|(name, size_bytes)| pb::AdminPartition { name, size_bytes })
.collect(),
holders: b.holders,
members: b.members,
raid_level: b.raid_level,
})
.collect(),
unresolved: s
.unresolved
.into_iter()
.map(|u| {
// A repository's storage folder that is not there yet is made at the first
// upload: no fault, and said so. Anything else unplaced keeps its reason.
let later = u.label.starts_with("storage:") && !exists(&u.path);
pb::AdminUnresolved {
label: u.label,
path: u.path.display().to_string(),
why: if later {
MADE_AT_FIRST_UPLOAD.to_string()
} else {
u.why
},
made_at_first_upload: later,
}
})
.collect(),
problems: s.problems,
floor_rule: FLOOR_RULE.to_string(),
}
}
const ZERO: disk_snapshot::Figures = disk_snapshot::Figures {
total_bytes: 0,
free_bytes: 0,
available_bytes: 0,
used_bytes: 0,
reserved_bytes: 0,
inodes_total: 0,
inodes_free: 0,
block_size: 0,
};
/// **Start the disk speed test** over `data_root`, once per process, on its
/// own thread (edda `disk_snapshot::speed`, 256 MiB written and removed). The
/// CLI calls it on an appliance's PID 1 or when [`SPEED_ENV`] is set; serving
/// never waits for it. Returns whether it started.
pub fn start_disk_speed(data_root: PathBuf) -> bool {
if SPEED_ROOT.set(data_root.clone()).is_err() {
return false;
}
// A32-14, stays a task: the disk speed probe (disk-snapshot), one bounded measurement.
disk_snapshot::speed::spawn(data_root, "holger-disk-speed", crate::clock::now_unix_ms)
}
/// The disk speed pane figure: the latched run, or why there is none.
pub fn disk_speed() -> pb::AdminDiskSpeed {
disk_speed_from(
SPEED_ROOT.get().map(PathBuf::as_path),
disk_snapshot::speed::last(),
)
}
/// Pure: the latch onto the wire. No run is `measured = false` with its reason.
pub fn disk_speed_from(
started: Option<&Path>,
last: Option<disk_snapshot::speed::Latched>,
) -> pb::AdminDiskSpeed {
use disk_snapshot::speed::Outcome;
let Some(l) = last else {
return match started {
None => pb::AdminDiskSpeed {
measured: false,
why: SPEED_NOT_RUN.into(),
..Default::default()
},
Some(p) => pb::AdminDiskSpeed {
measured: false,
path: p.display().to_string(),
why: SPEED_RUNNING.into(),
..Default::default()
},
};
};
let path = l.path.display().to_string();
match l.outcome {
Outcome::Done(r) => pb::AdminDiskSpeed {
measured: true,
path,
seq_write_bps: r.seq_write_bps,
seq_read_bps: r.seq_read_bps,
read_how: r.read_how.as_str().into(),
read_cached: r.read_was_cached(),
sync_4k_ops_per_s: r.sync_ops_per_s,
seconds: r.elapsed.as_secs_f64(),
at_unix_ms: l.at_unix_ms,
why: String::new(),
},
other => pb::AdminDiskSpeed {
measured: false,
path,
at_unix_ms: l.at_unix_ms,
why: format!("not measured: {}", other.row().0),
..Default::default()
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_volumes_are_system_data_and_every_storage_place_once() {
let mut places = crate::metering::StoragePlaces::new();
places.insert(
"crates".into(),
vec![
crate::repository::StoragePlace {
kind: "archive (writable)",
path: "/data/crates.zip".into(),
},
crate::repository::StoragePlace {
kind: "files",
path: "/data/crates".into(),
},
crate::repository::StoragePlace {
kind: "archive",
path: "/data/crates.zip".into(),
},
],
);
let v = volumes(Some(Path::new("/data")), &places);
let got: Vec<_> = v
.iter()
.map(|v| (v.label.as_str(), v.path.display().to_string()))
.collect();
assert_eq!(
got,
[
("system", "/".to_string()),
("data", "/data".into()),
("storage:crates", "/data/crates.zip".into()),
("storage:crates", "/data/crates".into())
]
);
}
/// ★ HOL-UI-FIX (owner 2026-11-01): a repository's storage folder that does not exist yet
/// is made at the first upload. The pane said "sits on no mount: No such file or
/// directory" for each, in warning yellow, on every fresh box; the truth is that nothing is
/// wrong. Any other unplaced path stays a fault with its reason.
#[test]
fn a_storage_folder_not_made_yet_is_created_at_the_first_upload() {
let snap = |label: &str, path: &str| disk_snapshot::Snapshot {
unresolved: vec![disk_snapshot::Unresolved {
label: label.into(),
path: path.into(),
why: format!("{path}: No such file or directory (os error 2)"),
}],
..Default::default()
};
let d = disks_from_with(snap("storage:incoming", "/data/store/incoming"), &|_| false);
let u = &d.unresolved[0];
assert!(
u.made_at_first_upload,
"a storage folder not made yet is made at the first upload"
);
assert_eq!(u.why, MADE_AT_FIRST_UPLOAD);
assert_eq!(
(u.label.as_str(), u.path.as_str()),
("storage:incoming", "/data/store/incoming")
);
// The data root missing is a fault, whatever exists.
let d = disks_from_with(snap("data", "/data"), &|_| false);
assert!(!d.unresolved[0].made_at_first_upload);
assert_eq!(
d.unresolved[0].why,
"/data: No such file or directory (os error 2)"
);
// A storage folder that IS there and sits on no mount is a fault too.
let d = disks_from_with(snap("storage:incoming", "/data/store/incoming"), &|_| true);
assert!(!d.unresolved[0].made_at_first_upload);
}
#[test]
fn an_unmeasured_mount_carries_its_reason_and_no_figure() {
let s = disk_snapshot::Snapshot {
disks: vec![disk_snapshot::DiskEntry {
mount: disk_snapshot::Mount {
source: "/dev/x".into(),
mount_point: "/x".into(),
fstype: "xfs".into(),
read_only: false,
},
labels: vec!["data".into()],
paths: vec!["/x".into()],
figures: Err("statvfs(/x) failed: EIO".into()),
floor_bytes: 0,
servable_bytes: 0,
}],
..Default::default()
};
let d = &disks_from(s).mounts[0];
assert_eq!(d.error, "statvfs(/x) failed: EIO");
assert_eq!((d.total_bytes, d.available_bytes, d.block_size), (0, 0, 0));
assert_eq!(d.labels, ["data"]);
}
#[test]
fn the_disk_speed_is_the_latched_run_or_why_there_is_none() {
use disk_snapshot::speed::{Latched, Outcome, ReadHow, Report};
let none = disk_speed_from(None, None);
assert!(!none.measured);
assert_eq!(none.why, SPEED_NOT_RUN);
assert_eq!(
(
none.seq_write_bps,
none.seq_read_bps,
none.sync_4k_ops_per_s
),
(0.0, 0.0, 0.0)
);
let running = disk_speed_from(Some(Path::new("/data")), None);
assert!(!running.measured);
assert_eq!(
(running.path.as_str(), running.why.as_str()),
("/data", SPEED_RUNNING)
);
let r = Report {
seq_write_bps: 812e6,
seq_read_bps: 1.9e9,
read_how: ReadHow::Direct,
sync_ops_per_s: 3100.0,
elapsed: std::time::Duration::from_millis(1600),
};
let done = disk_speed_from(
Some(Path::new("/data")),
Some(Latched {
outcome: Outcome::Done(r),
path: "/data".into(),
at_unix_ms: 7,
}),
);
assert!(done.measured && done.why.is_empty() && !done.read_cached);
assert_eq!(
(
done.seq_write_bps,
done.sync_4k_ops_per_s,
done.read_how.as_str(),
done.at_unix_ms
),
(812e6, 3100.0, "direct", 7)
);
let skipped = disk_speed_from(
Some(Path::new("/data")),
Some(Latched {
outcome: Outcome::Skipped {
free: 800 << 20,
need: 1 << 30,
},
path: "/data".into(),
at_unix_ms: 9,
}),
);
assert!(!skipped.measured);
assert!(
skipped.why.starts_with("not measured: skipped"),
"{}",
skipped.why
);
}
}