gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//! **The machine at a glance** (`StorageReality.glance`, owner 2026-10-04):
//! the four lines the console's Machine card shows — `/data`'s shape, the
//! off-stripe copies on `/serialdat`, the free space, the disk speed — each
//! with gunnar's own verdict.
//!
//! The console is a thin layer (owner, 2026-10-04): it draws these lines as
//! they come and decides none of them, so every verdict is made HERE, once,
//! and the CLI prints the same words.
//!
//! # The mislabel this replaces
//!
//! The console's Machine page drew `serialdat.dev` and `serialdat.class` as
//! "Backing device / Backing class" directly under "Data root" — so on every
//! estate it said `/data` was backed by `/dev/vda3 (p3)`, the boot disk's
//! third partition, which is `/serialdat`'s. `/data` is the stripe the
//! installer laid over the data disks (`data.shape`, `data.N.dev`). The two
//! are now two separate lines, each naming its own disk.

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";

/// Used share at which the free-space line turns amber and red: the marks
/// the estate's growth acts on (grow at 80 % used, emergency at 90 %).
pub const WARN_USED_PERCENT: u64 = 80;
pub const BAD_USED_PERCENT: u64 = 90;

/// Everything the four lines are read from — all of it already measured or
/// configured elsewhere; nothing here reads a disk.
#[derive(Debug, Clone, Default)]
pub struct Facts<'a> {
    /// The layout config is present (and verified or integrity-checked).
    pub record_present: bool,
    /// `data.mountpoint`, else the data root.
    pub data_label: &'a str,
    /// `data.shape`: `raid0` | `plain` | `p3` | "".
    pub data_shape: &'a str,
    /// `data.N.dev`, in order.
    pub data_devices: &'a [String],
    /// `serialdat.mountpoint`, else the configured slow root.
    pub serialdat_label: &'a str,
    /// `serialdat.class`: `p3` | `device` | `fallback` | "".
    pub serialdat_class: &'a str,
    /// `serialdat.dev`.
    pub serialdat_device: &'a str,
    /// The one off-stripe verdict: `off-stripe` | `same-device` | `absent`.
    pub off_stripe: &'a str,
    pub off_stripe_health: &'a [String],
    /// The data root's filesystem, measured on this call.
    pub data_fs: Option<&'a FilesystemUse>,
    /// The start-up disk test's outcome, `None` when it has not run.
    pub disk: Option<&'a crate::disktest::Outcome>,
}

/// `12.3 GiB`, binary, like every other byte figure an operator reads here.
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,
    }
}

/// **`/data`, from the installer's own record** — never from the mount table.
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."),
        ),
    }
}

/// **`/serialdat` — the off-stripe copies' own disk**, and the verdict on it.
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(" "),
    )
}

/// **Free space on the data root**, measured on this call.
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(),
        ),
    }
}

/// **The disk speed the start-up test measured.**
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(),
        ),
    }
}

/// The four lines, in the card's order.
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(),
        }
    }

    /// ★ **The mislabel, fixed**: `/data` names ITS devices, and `/dev/vda3`
    /// is said once — on the `/serialdat` line, as the boot disk's P3.
    #[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"]);
    }

    /// `same-device` and `absent` are never green; an unknown word is never
    /// green either.
    #[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");
    }

    /// Amber at 80 % used, red at 90 %; an unmeasured filesystem is unknown,
    /// never a number.
    #[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");
    }
}