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

nordisk/gunnar

deploy/upcloud/src/medium.rs

//! **Does this medium read its own CD? Its record says — or the answer is no.**
//!
//! `cargo xtask iso-installer` writes `<variant>.medium.json` beside every ISO
//! it composes (gunnar `xtask/src/medium.rs`): the ISO's sha256, the
//! korp-installer version it carries, the kernel it was given, and a verdict
//! `cd_driver` — true only when BOTH halves are proven (korp-installer ≥ 0.1.14,
//! which asks optical drives first, and a donor kernel with tunnr `939dbe3`'s
//! ATA/PIIX/AHCI/SR block).
//!
//! This module reads that record for the ISO about to be uploaded and answers
//! ONE question. Every doubt — no record, a record for another ISO (sha256
//! mismatch), a record that is not JSON, `cd_driver: false` — is "no": the
//! medium is uploaded twice, as it always was. That is the safe direction by
//! measurement: a medium WITHOUT the driver uploaded only as a cdrom sits in
//! `maintenance` for ever (T16's negative control on mock-upcloud-kvm), while a
//! medium WITH it uploaded twice merely costs the ~4 minutes it always did.

use std::path::{Path, PathBuf};

use crate::upload::Iso;

/// Where the record lives: beside the ISO, `<stem>.medium.json`.
pub fn record_path(iso: &Path) -> PathBuf {
    iso.with_extension("medium.json")
}

/// `Ok(why)` — this medium reads its own CD; `Err(why)` — upload it twice.
pub fn reads_own_cd(iso: &Iso) -> Result<String, String> {
    let path = record_path(&iso.path);
    let text = std::fs::read_to_string(&path)
        .map_err(|e| format!("no medium record at {} ({e})", path.display()))?;
    judge(&text, &iso.sha256).map_err(|e| format!("{}: {e}", path.display()))
}

/// The pure half: a record's text against the ISO's sha256.
pub fn judge(record: &str, iso_sha256: &str) -> Result<String, String> {
    let v: serde_json::Value =
        serde_json::from_str(record).map_err(|e| format!("not JSON ({e})"))?;
    let bound = v["iso_sha256"].as_str().unwrap_or("");
    if !bound.eq_ignore_ascii_case(iso_sha256) {
        return Err(format!(
            "the record is for ANOTHER medium (sha256 {bound:?}, this ISO is {iso_sha256}) — a record is \
             only evidence about the bytes it names"
        ));
    }
    let why = v["why"].as_str().unwrap_or("").to_string();
    match v["cd_driver"].as_bool() {
        Some(true) => Ok(why),
        Some(false) => Err(why),
        None => Err("the record carries no `cd_driver` verdict".to_string()),
    }
}

/// **The whole decision**: one upload only when the medium reads its own CD
/// AND powers off after INSTALL-OK. Without the power-off, the loop ejects the
/// cdrom mid-pass (the never-loop primitive), and a cd-only installer would
/// lose the medium it reads its payload from. Returns `(cd_only, the line)`.
pub fn decide(record: Result<String, String>, medium_powers_off: bool) -> (bool, String) {
    match (record, medium_powers_off) {
        (Ok(why), true) => (true, format!("ONE upload (cdrom only) — {why}")),
        (Ok(why), false) => (
            false,
            format!(
                "TWO uploads — the medium reads its own CD ({why}), but it does not power off after \
                 INSTALL-OK, so the cdrom is ejected mid-pass and the payload must also ride on virtio"
            ),
        ),
        (Err(why), _) => (false, format!("TWO uploads (cdrom + virtio) — {why}")),
    }
}

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

    fn record(sha: &str, cd: bool) -> String {
        serde_json::json!({"iso_sha256": sha, "korp_installer": "0.1.14", "cd_driver": cd, "why": if cd { "reads optical first" } else { "no CD block" }}).to_string()
    }

    #[test]
    fn a_record_for_this_medium_saying_yes_is_yes() {
        assert_eq!(
            judge(&record("ABCD", true), "abcd").unwrap(),
            "reads optical first"
        );
    }

    /// Every doubt is "upload it twice".
    #[test]
    fn every_doubt_is_two_uploads() {
        assert_eq!(
            judge(&record("abcd", false), "abcd").unwrap_err(),
            "no CD block"
        );
        assert!(
            judge(&record("ffff", true), "abcd")
                .unwrap_err()
                .contains("ANOTHER medium"),
            "a yes about other bytes is no"
        );
        assert!(judge("{not json", "abcd").unwrap_err().contains("not JSON"));
        assert!(judge(r#"{"iso_sha256":"abcd"}"#, "abcd")
            .unwrap_err()
            .contains("no `cd_driver`"));
    }

    /// One upload needs BOTH: the driver (the record) and the power-off.
    #[test]
    fn one_upload_needs_the_driver_and_the_power_off() {
        assert!(decide(Ok("yes".into()), true).0);
        let (one, line) = decide(Ok("yes".into()), false);
        assert!(!one && line.contains("ejected mid-pass"), "{line}");
        assert!(!decide(Err("no record".into()), true).0);
        assert!(!decide(Err("no record".into()), false).0);
    }

    #[test]
    fn the_record_sits_beside_the_iso() {
        assert_eq!(
            record_path(Path::new("/g/.iso-out/gunnar-installer-metered.iso")),
            PathBuf::from("/g/.iso-out/gunnar-installer-metered.medium.json")
        );
    }
}