Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-server/src/api/disks.rs

//! **Every disk, for the administrator** — `GET /api/disks`.
//!
//! The data set (used / free / floor / servable, and its members), the
//! system disk, every other storage mount, the block devices under them, the
//! exploded read cache (its size, its budget and its share of the data set),
//! and each twin's volume — on one admin read.
//!
//! # Admin, and enforced here, before gunnar is dialled
//!
//! `/api/disks` is on `role_floor`'s `ADMIN_READS`, beside `/api/machine` and
//! for the same reason: device names, md members and every mount point on the
//! box are its hardware identity, not any tenant's business. A viewer or an
//! operator is refused by the middleware, so the handler never runs and no
//! call reaches gunnar. A tenant's OWN usage is `/api/namespaces/{ns}/usage`,
//! answered with their own key (and 404 for anybody else's, `api::absent`).
//!
//! # Three calls, each allowed to fail alone
//!
//! * `Admin.Capacity { disks: true }` — the data set's figures and, from a
//!   gunnar that has it (CapacityResponse field 27, gunnar branch
//!   `ui-404-admin-disks`), every mount and block device, measured by edda's
//!   `disk-snapshot` on the box. A gunnar older than the field answers the data
//!   set and no disks, and the page says so by name.
//! * `Admin.StorageInfo` — the explode read cache's bytes and budget.
//! * `Admin.Health` — each twin's reported volume.
//!
//! A source that does not answer is a line in `unanswered`, in gunnar's words,
//! and its part of the page is empty rather than zero. Nothing here is derived
//! that the box did not measure, with one exception said out loud: the read
//! cache's `share` is its bytes over the data set's used bytes, two readings
//! of the same box a moment apart.
//!
//! # The read cache's hysteresis is not reported yet, and that is carried
//!
//! znippy-plugin-git 0.1.6 brings the 80 % / 75 % high/low-water state of the
//! explode cache; the gunnar this console talks to today (plugin 0.1.5) exposes
//! only the bytes and the budget. `hysteresis` is therefore always absent and
//! `hysteresis_note` says why, so the page never draws a state nobody measured.

use axum::extract::State;
use axum::Json;
use serde::Serialize;

use crate::api::ApiResult;
use crate::app::AppState;
use crate::pb;

/// Why the read cache has no hysteresis state on this page.
pub const HYSTERESIS_NOT_REPORTED: &str = "Gunnar does not report the read cache's 80 % / 75 % \
     high/low-water state yet; it arrives with znippy-plugin-git 0.1.6";

/// Why there are no per-disk rows from an older gunnar.
pub const NO_DISKS: &str = "this Gunnar answers Admin.Capacity without `disks`: it is older than \
     the per-disk report (CapacityResponse field 27), so only the data set is shown";

/// `GET /api/disks`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Disks {
    /// The store's data set, `Admin.Capacity`'s own figures. `None` when that
    /// call did not answer.
    pub data_set: Option<DataSet>,
    /// The mount gunnar labels `system` (`/`). `None` when not reported.
    pub system: Option<Mount>,
    /// Every storage mount on the box, the data set's and the system's
    /// included, in mount-table order.
    pub mounts: Vec<Mount>,
    /// Every `/sys/block` device.
    pub devices: Vec<Device>,
    /// The exploded read cache. `None` when `Admin.StorageInfo` did not answer.
    pub read_cache: Option<ReadCache>,
    /// Each twin's volume, as its heartbeat reports it. Empty is "no twin
    /// reported" only when `Admin.Health` answered (see `unanswered`).
    pub twins: Vec<TwinDisk>,
    /// When the disks were measured, the box's clock.
    pub measured_at_unix_ms: Option<i64>,
    /// What the box could not read or place, in its words.
    pub problems: Vec<String>,
    /// The sources that did not answer, in words. Their parts are empty.
    pub unanswered: Vec<String>,
    /// **The server's MEASURED disk speed**: the start-up disk test, with the
    /// bytes and the time behind each rate. `None` when `Admin.Capacity` did
    /// not answer.
    pub disk_speed: Option<DiskSpeed>,
    /// **Write amplification**: bytes that land on disk per byte pushed.
    /// Always reported as not measured today, with why — see [`NotMeasured`].
    pub write_amplification: NotMeasured,
    /// **The priority transfer** (used / limit over 30 days, class, queue
    /// length) from edda's `admit` trait. Not in the gunnar this console talks
    /// to, so not measured, with why.
    pub priority_transfer: NotMeasured,
    /// **/serialdat** — the system disk's P3 (metadata copy, crash record,
    /// logs, audit trail, grow journal): its mount and what is on it.
    pub serialdat: Option<Serialdat>,
    /// The same, and every disk, on the TWIN (`GUNNAR_UI_TWIN`), asked with
    /// the console's key. `None` when no twin is configured.
    pub twin_box: Option<TwinBox>,
    /// Things an administrator must act on: a /serialdat past 80 % (here or
    /// on the twin).
    pub warnings: Vec<String>,
}

/// /serialdat: the mount's statvfs and the breakdown by known path.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Serialdat {
    pub mount: Mount,
    pub breakdown: Vec<PathRow>,
}

/// One known path on /serialdat (or `other`).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct PathRow {
    pub label: String,
    pub path: String,
    /// `None` when the walk failed (`error` says why) — never a 0.
    pub bytes: Option<u64>,
    pub error: String,
}

/// The twin's disks, as its own gunnar reports them.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct TwinBox {
    pub address: String,
    pub mounts: Vec<Mount>,
    pub serialdat: Option<Serialdat>,
    /// Why there is nothing, in words, when the twin did not answer.
    pub unanswered: String,
}

/// /serialdat's warning line: past this share of its mount, an admin acts.
pub const SERIALDAT_WARN_PERMILLE: u32 = 800;

fn serialdat_of(mounts: &[Mount], breakdown: Vec<pb::PathUse>) -> Option<Serialdat> {
    let mount = mounts.iter().find(|m| m.labels.iter().any(|l| l == "serialdat"))?.clone();
    Some(Serialdat {
        mount,
        breakdown: breakdown
            .into_iter()
            .map(|p| PathRow {
                bytes: p.measured.then_some(p.bytes),
                label: p.label,
                path: p.path,
                error: p.error,
            })
            .collect(),
    })
}

fn serialdat_warning(where_: &str, s: &Serialdat) -> Option<String> {
    let permille = s.mount.used_permille?;
    (permille >= SERIALDAT_WARN_PERMILLE).then(|| {
        format!(
            "{where_}/serialdat ({}) is {}.{} % full — past the 80 % line; the metadata copy, \
             crash record and audit trail live there",
            s.mount.mount_point,
            permille / 10,
            permille % 10
        )
    })
}

/// The twin's answer to the same `Admin.Capacity { disks: true }`.
pub fn render_twin(address: &str, answer: Result<pb::CapacityResponse, String>) -> TwinBox {
    match answer {
        Ok(c) => match c.disks {
            Some(snap) => {
                let mounts: Vec<Mount> = snap.mounts.into_iter().map(mount).collect();
                let serialdat = serialdat_of(&mounts, snap.serialdat);
                TwinBox { address: address.to_owned(), mounts, serialdat, unanswered: String::new() }
            }
            None => TwinBox { address: address.to_owned(), unanswered: NO_DISKS.to_owned(), ..TwinBox::default() },
        },
        Err(why) => TwinBox {
            address: address.to_owned(),
            unanswered: format!("the twin's Admin.Capacity: {why}"),
            ..TwinBox::default()
        },
    }
}

/// The start-up disk test, as the box reports it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct DiskSpeed {
    /// `measured` | `not_run` | `skipped` | `failed` | `unknown`.
    pub test: &'static str,
    pub seq_write_bps: Option<u64>,
    /// Absent when the read came back from the page cache (`cached`).
    pub seq_read_bps: Option<u64>,
    pub sync_milliops_per_s: Option<u64>,
    /// `direct` | `dropped`.
    pub read_how: String,
    /// gunnar's own sentence. Never empty from a gunnar that has the test.
    pub reason: String,
    /// What the rates are made of (CapacityResponse 28-33): the bytes, each
    /// leg's clock, the fsync'd operations. `None` from a gunnar older than
    /// the fields, or when nothing was measured.
    pub bytes: Option<u64>,
    pub write_us: Option<u64>,
    pub read_us: Option<u64>,
    pub sync_ops: Option<u64>,
    pub sync_us: Option<u64>,
    pub run_us: Option<u64>,
}

/// **A figure this box does not measure — shown as N/A, never 0.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct NotMeasured {
    /// Always `None` today; the field exists so the day a gunnar reports it
    /// the page has a place for it, and so a JSON reader cannot mistake the
    /// absence for a zero.
    pub value: Option<String>,
    pub why: &'static str,
}

/// Why write amplification is N/A.
pub const WRITE_AMPLIFICATION_NOT_MEASURED: &str = "Gunnar counts the bytes pushed (Usage) but not      the bytes that reach the disk for them (transaction log + packs), so the ratio cannot be      measured on this box; a push is known to land about 4.6 times its size, but that is a lab      figure, not this box's";

/// Why the priority transfer is N/A.
pub const PRIORITY_TRANSFER_NOT_MEASURED: &str = "the priority transfer (used / limit over 30      days, class, queue length) comes from edda's admit trait, which the running Gunnar does not      carry yet (or carries as PassThrough, which measures nothing)";

fn disk_test_word(code: i32) -> &'static str {
    match pb::DiskTest::try_from(code) {
        Ok(pb::DiskTest::DisktestMeasured) => "measured",
        Ok(pb::DiskTest::DisktestNotRun) => "not_run",
        Ok(pb::DiskTest::DisktestSkipped) => "skipped",
        Ok(pb::DiskTest::DisktestFailed) => "failed",
        _ => "unknown",
    }
}

fn disk_speed(c: &pb::CapacityResponse) -> DiskSpeed {
    DiskSpeed {
        test: disk_test_word(c.disk_test),
        seq_write_bps: c.disk_seq_write_bps,
        seq_read_bps: c.disk_seq_read_bps,
        sync_milliops_per_s: c.disk_sync_milliops_per_s,
        read_how: c.disk_read_how.clone(),
        reason: c.disk_reason.clone(),
        bytes: c.disk_test_bytes,
        write_us: c.disk_write_us,
        read_us: c.disk_read_us,
        sync_ops: c.disk_sync_ops,
        sync_us: c.disk_sync_us,
        run_us: c.disk_test_us,
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct DataSet {
    pub root: String,
    pub total_bytes: u64,
    pub free_bytes: u64,
    pub used_bytes: u64,
    /// The `--disk-floor` in force: exists, never sold.
    pub floor_bytes: u64,
    pub servable_bytes: u64,
    /// gunnar's words for the layout: `4×75G raid0 xfs`.
    pub layout: String,
    pub growth: &'static str,
    /// The block device under the store root and its md members, when the
    /// disk report places it. Empty from an older gunnar or a plain disk.
    pub device: String,
    pub raid_level: String,
    pub members: Vec<Member>,
}

/// One member of the data set. Its SIZE is measured; its use is not a
/// separate reading — a striped set is used as a whole — so none is invented.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Member {
    pub name: String,
    /// `None` when the member is named by md but its size was not found.
    pub size_bytes: Option<u64>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Mount {
    pub source: String,
    pub mount_point: String,
    pub fs_type: String,
    pub read_only: bool,
    pub labels: Vec<String>,
    pub measured: bool,
    pub error: String,
    /// The figures below are `None` when `measured` is false: a number that
    /// was not read is never shown as one.
    pub total_bytes: Option<u64>,
    pub free_bytes: Option<u64>,
    pub available_bytes: Option<u64>,
    pub used_bytes: Option<u64>,
    pub reserved_bytes: Option<u64>,
    pub floor_bytes: Option<u64>,
    pub servable_bytes: Option<u64>,
    pub used_permille: Option<u32>,
    pub inodes_total: Option<u64>,
    pub inodes_free: Option<u64>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct Device {
    pub name: String,
    pub size_bytes: u64,
    pub rotational: bool,
    pub removable: bool,
    pub read_only: bool,
    pub partitions: Vec<(String, u64)>,
    pub holders: Vec<String>,
    pub members: Vec<String>,
    pub raid_level: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ReadCache {
    /// Exploded bytes on disk now (`StorageInfoResponse.total_explode_bytes`).
    pub bytes: u64,
    /// Their summed budget; 0 = unbounded.
    pub budget_bytes: u64,
    /// `bytes / budget`, ‰. `None` when unbounded.
    pub of_budget_permille: Option<u32>,
    /// `bytes / data set used`, ‰. `None` when the data set did not answer or
    /// holds nothing.
    pub of_data_used_permille: Option<u32>,
    /// The 80 % / 75 % high/low-water state. Always `None` today; see
    /// `hysteresis_note`.
    pub hysteresis: Option<String>,
    pub hysteresis_note: &'static str,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct TwinDisk {
    pub principal: String,
    pub address: String,
    pub state: String,
    /// `None` when the twin did not measure its volume (total 0).
    pub total_bytes: Option<u64>,
    pub used_bytes: Option<u64>,
    pub fill_permille: Option<u32>,
}

fn permille(part: u64, whole: u64) -> Option<u32> {
    (whole > 0).then(|| ((u128::from(part) * 1000) / u128::from(whole)).min(u128::from(u32::MAX)) as u32)
}

fn mount(m: pb::DiskMount) -> Mount {
    let read = |v: u64| m.measured.then_some(v);
    Mount {
        total_bytes: read(m.total_bytes),
        free_bytes: read(m.free_bytes),
        available_bytes: read(m.available_bytes),
        used_bytes: read(m.used_bytes),
        reserved_bytes: read(m.reserved_bytes),
        floor_bytes: read(m.floor_bytes),
        servable_bytes: read(m.servable_bytes),
        used_permille: if m.measured { permille(m.used_bytes, m.total_bytes) } else { None },
        inodes_total: read(m.inodes_total),
        inodes_free: read(m.inodes_free),
        source: m.source,
        mount_point: m.mount_point,
        fs_type: m.fs_type,
        read_only: m.read_only,
        labels: m.labels,
        measured: m.measured,
        error: m.error,
    }
}

fn device(d: pb::BlockDevice) -> Device {
    Device {
        name: d.name,
        size_bytes: d.size_bytes,
        rotational: d.rotational,
        removable: d.removable,
        read_only: d.read_only,
        partitions: d.partitions.into_iter().map(|p| (p.name, p.size_bytes)).collect(),
        holders: d.holders,
        members: d.members,
        raid_level: d.raid_level,
    }
}

/// The size of a device or partition named `name`, from the device table.
fn size_of(devices: &[Device], name: &str) -> Option<u64> {
    devices
        .iter()
        .find(|d| d.name == name)
        .map(|d| d.size_bytes)
        .or_else(|| {
            devices
                .iter()
                .flat_map(|d| d.partitions.iter())
                .find(|(p, _)| p == name)
                .map(|(_, size)| *size)
        })
}

/// **The one reader of the three answers.** Pure, so every rule above is a
/// test and not a hope.
pub fn render(
    capacity: Result<pb::CapacityResponse, String>,
    storage: Result<pb::StorageInfoResponse, String>,
    health: Result<pb::HealthResponse, String>,
) -> Disks {
    let mut out = Disks {
        write_amplification: NotMeasured { value: None, why: WRITE_AMPLIFICATION_NOT_MEASURED },
        priority_transfer: NotMeasured { value: None, why: PRIORITY_TRANSFER_NOT_MEASURED },
        ..Disks::default()
    };
    match capacity {
        Ok(c) => {
            out.disk_speed = Some(disk_speed(&c));
            let used = c.total_bytes.saturating_sub(c.free_bytes);
            let mut set = DataSet {
                root: c.root.clone(),
                total_bytes: c.total_bytes,
                free_bytes: c.free_bytes,
                used_bytes: used,
                floor_bytes: c.floor_bytes,
                servable_bytes: c.servable_bytes,
                layout: c.data_set.clone(),
                growth: crate::api::usage::growth_word(c.growth),
                ..DataSet::default()
            };
            match c.disks {
                Some(snap) => {
                    out.measured_at_unix_ms = (snap.measured_at_unix_ms > 0).then_some(snap.measured_at_unix_ms);
                    out.mounts = snap.mounts.into_iter().map(mount).collect();
                    out.devices = snap.devices.into_iter().map(device).collect();
                    out.problems = snap.problems;
                    out.problems.extend(
                        snap.unresolved
                            .into_iter()
                            .map(|u| format!("{} ({}) is on no mount: {}", u.label, u.path, u.why)),
                    );
                    out.system = out.mounts.iter().find(|m| m.labels.iter().any(|l| l == "system")).cloned();
                    out.serialdat = serialdat_of(&out.mounts, snap.serialdat);
                    if let Some(w) = out.serialdat.as_ref().and_then(|s| serialdat_warning("", s)) {
                        out.warnings.push(w);
                    }
                    // The block device under the store root, and its members.
                    if let Some(data) = out.mounts.iter().find(|m| m.labels.iter().any(|l| l == "data")) {
                        let name = data.source.rsplit('/').next().unwrap_or_default().to_owned();
                        if let Some(dev) = out.devices.iter().find(|d| d.name == name) {
                            set.device = dev.name.clone();
                            set.raid_level = dev.raid_level.clone();
                            set.members = dev
                                .members
                                .iter()
                                .map(|m| Member {
                                    name: m.clone(),
                                    size_bytes: size_of(&out.devices, m),
                                })
                                .collect();
                        } else {
                            set.device = name;
                        }
                    }
                }
                None => out.unanswered.push(NO_DISKS.to_owned()),
            }
            out.data_set = Some(set);
        }
        Err(why) => out.unanswered.push(format!("Admin.Capacity: {why}")),
    }
    match storage {
        Ok(s) => {
            let used = out.data_set.as_ref().map(|d| d.used_bytes).unwrap_or(0);
            out.read_cache = Some(ReadCache {
                bytes: s.total_explode_bytes,
                budget_bytes: s.total_explode_budget_bytes,
                of_budget_permille: permille(s.total_explode_bytes, s.total_explode_budget_bytes),
                of_data_used_permille: permille(s.total_explode_bytes, used),
                hysteresis: None,
                hysteresis_note: HYSTERESIS_NOT_REPORTED,
            });
        }
        Err(why) => out.unanswered.push(format!("Admin.StorageInfo: {why}")),
    }
    match health {
        Ok(h) => {
            out.twins = h
                .twins
                .into_iter()
                .map(|t| {
                    let measured = t.disk_total_bytes > 0;
                    TwinDisk {
                        principal: t.principal,
                        address: t.address,
                        state: t.state,
                        total_bytes: measured.then_some(t.disk_total_bytes),
                        used_bytes: measured.then_some(t.disk_used_bytes),
                        fill_permille: measured.then_some(t.fill_permille),
                    }
                })
                .collect();
        }
        Err(why) => out.unanswered.push(format!("Admin.Health: {why}")),
    }
    out
}

fn words(status: tonic::Status) -> String {
    if status.message().trim().is_empty() {
        format!("{:?}", status.code())
    } else {
        status.message().to_owned()
    }
}

/// `GET /api/disks` — admin only (`role_floor`'s `ADMIN_READS`). Not cached:
/// an administrator reloading this page is looking for a number that moved.
pub async fn disks(
    reader: crate::api::Reader,
    State(state): State<AppState>,
) -> ApiResult<Disks> {
    let (mut a, mut b, mut c) = (reader.gunnar.admin(), reader.gunnar.admin(), reader.gunnar.admin());
    let (capacity, storage, health) = tokio::join!(
        a.capacity(pb::CapacityRequest {
            requested_bytes: 0,
            namespace: String::new(),
            disks: true,
        }),
        b.storage_info(pb::StorageInfoRequest { repo: None }),
        c.health(pb::HealthRequest {}),
    );
    let mut out = render(
        capacity.map(tonic::Response::into_inner).map_err(words),
        storage.map(tonic::Response::into_inner).map_err(words),
        health.map(tonic::Response::into_inner).map_err(words),
    );
    // The twin's own disks, when one is configured: the same question to its
    // gunnar. A failure is the twin's line, never the page's.
    if let Some(twin) = state.twin.as_ref() {
        let answer = twin
            .gunnar
            .admin()
            .capacity(pb::CapacityRequest { requested_bytes: 0, namespace: String::new(), disks: true })
            .await
            .map(tonic::Response::into_inner)
            .map_err(words);
        let tb = render_twin(&twin.address, answer);
        if let Some(w) = tb.serialdat.as_ref().and_then(|s| serialdat_warning("the twin's ", s)) {
            out.warnings.push(w);
        }
        out.twin_box = Some(tb);
    }
    Ok(Json(out))
}

#[cfg(test)]
mod tests {
    use super::*;

    const GIB: u64 = 1 << 30;

    fn snapshot() -> pb::DiskSnapshot {
        pb::DiskSnapshot {
            measured_at_unix_ms: 1_790_000_000_000,
            mounts: vec![
                pb::DiskMount {
                    source: "/dev/vda2".into(),
                    mount_point: "/".into(),
                    fs_type: "ext4".into(),
                    labels: vec!["system".into()],
                    measured: true,
                    total_bytes: 10 * GIB,
                    free_bytes: 6 * GIB,
                    available_bytes: 5 * GIB,
                    used_bytes: 4 * GIB,
                    reserved_bytes: GIB,
                    floor_bytes: 2 * GIB,
                    servable_bytes: 3 * GIB,
                    ..Default::default()
                },
                pb::DiskMount {
                    source: "/dev/md0".into(),
                    mount_point: "/data".into(),
                    fs_type: "xfs".into(),
                    labels: vec!["data".into()],
                    measured: true,
                    total_bytes: 300 * GIB,
                    free_bytes: 200 * GIB,
                    available_bytes: 200 * GIB,
                    used_bytes: 100 * GIB,
                    floor_bytes: 15 * GIB,
                    servable_bytes: 185 * GIB,
                    ..Default::default()
                },
                pb::DiskMount {
                    source: "/dev/vdz1".into(),
                    mount_point: "/broken".into(),
                    fs_type: "xfs".into(),
                    measured: false,
                    error: "statvfs(/broken): EIO".into(),
                    ..Default::default()
                },
            ],
            devices: vec![
                pb::BlockDevice { name: "md0".into(), size_bytes: 300 * GIB, members: vec!["vdb1".into(), "vdc1".into(), "vdq9".into()], raid_level: "raid0".into(), ..Default::default() },
                pb::BlockDevice { name: "vdb".into(), size_bytes: 151 * GIB, partitions: vec![pb::BlockPartition { name: "vdb1".into(), size_bytes: 150 * GIB }], holders: vec!["md0".into()], ..Default::default() },
                pb::BlockDevice { name: "vdc".into(), size_bytes: 151 * GIB, partitions: vec![pb::BlockPartition { name: "vdc1".into(), size_bytes: 150 * GIB }], holders: vec!["md0".into()], ..Default::default() },
            ],
            unresolved: vec![pb::UnresolvedDisk { label: "slow".into(), path: "/serialdat".into(), why: "no such file".into() }],
            problems: vec![],
            serialdat: vec![],
        }
    }

    fn capacity(disks: Option<pb::DiskSnapshot>) -> pb::CapacityResponse {
        pb::CapacityResponse {
            root: "/data".into(),
            total_bytes: 300 * GIB,
            free_bytes: 200 * GIB,
            floor_bytes: 15 * GIB,
            servable_bytes: 185 * GIB,
            data_set: "2×150G raid0 xfs".into(),
            growth: pb::SetGrowth::GrowthSealed as i32,
            disks,
            ..Default::default()
        }
    }

    fn health() -> pb::HealthResponse {
        pb::HealthResponse {
            twins: vec![
                pb::TwinHeartbeat { principal: "twin".into(), address: "10.13.8.122:40000".into(), disk_total_bytes: 400 * GIB, disk_used_bytes: 100 * GIB, fill_permille: 250, state: "ok".into(), ..Default::default() },
                pb::TwinHeartbeat { principal: "silent".into(), state: "silent".into(), ..Default::default() },
            ],
            ..Default::default()
        }
    }

    #[test]
    fn every_disk_the_read_cache_and_the_twins_on_one_answer() {
        let storage = pb::StorageInfoResponse { total_explode_bytes: 20 * GIB, total_explode_budget_bytes: 40 * GIB, ..Default::default() };
        let d = render(Ok(capacity(Some(snapshot()))), Ok(storage), Ok(health()));
        assert!(d.unanswered.is_empty(), "{:?}", d.unanswered);

        let set = d.data_set.as_ref().expect("the data set");
        assert_eq!((set.total_bytes, set.used_bytes, set.free_bytes, set.floor_bytes, set.servable_bytes), (300 * GIB, 100 * GIB, 200 * GIB, 15 * GIB, 185 * GIB));
        assert_eq!((set.device.as_str(), set.raid_level.as_str(), set.growth), ("md0", "raid0", "sealed"));
        assert_eq!(
            set.members,
            vec![
                Member { name: "vdb1".into(), size_bytes: Some(150 * GIB) },
                Member { name: "vdc1".into(), size_bytes: Some(150 * GIB) },
                // Named by md, not found in the table: no size is invented.
                Member { name: "vdq9".into(), size_bytes: None },
            ]
        );

        let system = d.system.as_ref().expect("the system disk");
        assert_eq!((system.mount_point.as_str(), system.used_bytes, system.used_permille), ("/", Some(4 * GIB), Some(400)));
        assert_eq!(d.mounts.len(), 3);
        let broken = &d.mounts[2];
        assert!(!broken.measured && broken.total_bytes.is_none() && broken.used_permille.is_none() && broken.error.contains("EIO"), "{broken:?}");
        assert_eq!(d.devices.len(), 3);
        assert!(d.problems.iter().any(|p| p.contains("slow") && p.contains("no such file")), "{:?}", d.problems);

        let cache = d.read_cache.as_ref().expect("the read cache");
        assert_eq!((cache.bytes, cache.budget_bytes, cache.of_budget_permille, cache.of_data_used_permille), (20 * GIB, 40 * GIB, Some(500), Some(200)));
        assert!(cache.hysteresis.is_none() && cache.hysteresis_note.contains("0.1.6"));

        assert_eq!(d.twins[0].fill_permille, Some(250));
        assert_eq!((d.twins[1].total_bytes, d.twins[1].fill_permille), (None, None), "an unmeasured twin volume is not 0");

        // N/A, never 0, for what this box does not measure.
        assert!(d.write_amplification.value.is_none() && d.write_amplification.why.contains("4.6"));
        assert!(d.priority_transfer.value.is_none() && d.priority_transfer.why.contains("admit"));
    }

    /// The measured disk speed, with the bytes and the time behind each rate.
    #[test]
    fn the_disk_speed_carries_its_bytes_and_clocks_and_an_unrun_test_carries_none() {
        let c = pb::CapacityResponse {
            disk_test: pb::DiskTest::DisktestMeasured as i32,
            disk_seq_write_bps: Some(812_000_000),
            disk_seq_read_bps: None,
            disk_sync_milliops_per_s: Some(3_100_400),
            disk_read_how: "direct".into(),
            disk_reason: "This disk, at start-up …".into(),
            disk_test_bytes: Some(256 << 20),
            disk_write_us: Some(330_000),
            disk_read_us: Some(20_000),
            disk_sync_ops: Some(1550),
            disk_sync_us: Some(500_000),
            disk_test_us: Some(1_600_000),
            ..capacity(None)
        };
        let s = render(Ok(c), Err("x".into()), Err("x".into())).disk_speed.expect("speed");
        assert_eq!(s.test, "measured");
        assert_eq!((s.seq_write_bps, s.seq_read_bps), (Some(812_000_000), None), "a cached read is absent, not fast");
        assert_eq!((s.bytes, s.write_us, s.read_us, s.sync_ops, s.sync_us, s.run_us), (Some(256 << 20), Some(330_000), Some(20_000), Some(1550), Some(500_000), Some(1_600_000)));

        let s = render(Ok(capacity(None)), Err("x".into()), Err("x".into())).disk_speed.expect("speed");
        assert_eq!(s.test, "unknown");
        assert_eq!((s.seq_write_bps, s.bytes, s.run_us), (None, None, None));
    }

    /// /serialdat: its mount, the breakdown (an unread path is None, not 0),
    /// the 80 % warning — here and on the twin.
    #[test]
    fn serialdat_is_broken_down_and_warned_about_past_eighty_percent() {
        let mut snap = snapshot();
        snap.mounts.push(pb::DiskMount {
            source: "/dev/vda3".into(),
            mount_point: "/serialdat".into(),
            labels: vec!["serialdat".into()],
            measured: true,
            total_bytes: 10 * GIB,
            used_bytes: 17 * GIB / 2,
            ..Default::default()
        });
        snap.serialdat = vec![
            pb::PathUse { label: "audit trail".into(), path: "/serialdat/log/audit-chain.jsonl".into(), measured: true, bytes: GIB, error: String::new() },
            pb::PathUse { label: "crash record".into(), path: "/serialdat/crash".into(), measured: false, bytes: 0, error: "EACCES".into() },
            pb::PathUse { label: "other".into(), measured: true, bytes: 15 * GIB / 2, ..Default::default() },
        ];
        let d = render(Ok(capacity(Some(snap.clone()))), Err("x".into()), Err("x".into()));
        let s = d.serialdat.as_ref().expect("serialdat");
        assert_eq!(s.mount.used_permille, Some(850));
        assert_eq!(s.breakdown[0].bytes, Some(GIB));
        assert_eq!((s.breakdown[1].bytes, s.breakdown[1].error.as_str()), (None, "EACCES"), "an unread path is not 0");
        assert!(d.warnings.iter().any(|w| w.contains("85.0 %") && w.contains("80 %")), "{:?}", d.warnings);

        let t = render_twin("10.13.8.122:50051", Ok(capacity(Some(snap))));
        assert!(t.serialdat.is_some() && t.unanswered.is_empty());
        let t = render_twin("10.13.8.122:50051", Err("unreachable".into()));
        assert!(t.mounts.is_empty() && t.unanswered.contains("unreachable"));

        // Below 80 %: no warning.
        let d = render(Ok(capacity(Some(snapshot()))), Err("x".into()), Err("x".into()));
        assert!(d.warnings.is_empty());
    }

    #[test]
    fn an_older_gunnar_and_a_silent_source_are_said_in_words_not_drawn_as_zero() {
        let d = render(Ok(capacity(None)), Err("unimplemented".into()), Err("Admin.Health is not served".into()));
        assert!(d.data_set.is_some(), "the data set still answers");
        assert!(d.mounts.is_empty() && d.system.is_none() && d.read_cache.is_none() && d.twins.is_empty());
        assert!(d.unanswered.iter().any(|u| u == NO_DISKS));
        assert!(d.unanswered.iter().any(|u| u.starts_with("Admin.StorageInfo")));
        assert!(d.unanswered.iter().any(|u| u.starts_with("Admin.Health")));

        let d = render(Err("gunnar is down".into()), Ok(pb::StorageInfoResponse { total_explode_bytes: 5, ..Default::default() }), Ok(pb::HealthResponse::default()));
        assert!(d.data_set.is_none());
        let cache = d.read_cache.expect("the cache still answers");
        assert_eq!((cache.of_budget_permille, cache.of_data_used_permille), (None, None), "no budget and no data set: no share invented");
    }
}