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/machine.rs

//! **Which machine this door deploys to, and what that machine implies about
//! attaching a medium** — derived from the roster, never remembered.
//!
//! # The fact this module exists to stop restating
//!
//! `korp-installer` probes **only virtio** block devices for the ISO-9660
//! `CD001` magic. The appliance kernel is the `nuked` preset and has no
//! ATA/AHCI/SCSI at all, so `sr0` does not exist for it to look at even if it
//! wanted to. But the firmware — UpCloud is SeaBIOS-only — boots from a
//! **cdrom**. So one ISO has to ride as two devices, and a medium attached only
//! as a cdrom boots the kernel, starts PID 1 and dies with
//!
//! ```text
//! INSTALLER: volume: no block device carries the ISO-9660 magic (CD001) …
//! INSTALLER: FAILED payload: /run/installer/BOOTX64.EFI: No such file or directory
//! ```
//!
//! That fact was written down FIVE times before this file existed — draupnir
//! `519afcf`'s `MediumAttach::{Cdrom,Virtio,Both}`, `installtest/Cargo.toml`'s
//! MEASURED 2026-08-15 note quoting the identical `CD001` line,
//! `installtest/src/mother_kvm.rs`'s "cdrom AND read-only virtio", this crate's
//! own header, and the roster's own `MediumBring::IsoCdrom` — and the cloud
//! path still had to learn it by hand, against the live box, at the cost of an
//! hour. `plan.rs` then encoded it a sixth time as two hardcoded
//! `client.attach` calls: correct, and correct for no reason a reader or a
//! second platform could use.
//!
//! # What deriving it buys
//!
//! The roster ([`nornir_build_thing::target::presets`]) is the one table of
//! named machines in the constellation, and every row says how its platform
//! takes a medium. Reading that word instead of restating it means:
//!
//! * The two attaches are a CONSEQUENCE of `upcloud` being `IsoCdrom`, so a
//!   reader of `plan.rs` is pointed at the fact rather than told the answer.
//! * `scaleway-instance` — whose device set is IDENTICAL to UpCloud's,
//!   virtio-blk + virtio-net, so no shape check could ever tell them apart — is
//!   **refused by name**, because its `MediumBring` is `ImportQcow2` and this
//!   verb's whole premise (attach an ISO to a running server) does not exist
//!   there. Before, it would have been attempted: two storages uploaded, a live
//!   appliance stopped, and a failure three API calls into the destructive
//!   window.
//! * A machine added to the roster later joins this door with no edit here.
//!
//! # Why the answer is an enum and not a pair of bools
//!
//! [`Attach`] has exactly ONE variant today, and that is the honest count: one
//! attach shape has ever been executed by this procedure, against one machine.
//! It is an enum rather than a `bool` so that the day the roster grows a
//! platform this door can serve with a different face count, the `match` in
//! [`Attach::faces`] fails to compile until somebody decides — instead of a
//! second hardcoded pair appearing somewhere else.

use nornir_build_thing::target::presets;

use crate::api::DeviceKind;

/// **The machine this door deploys to.** `deploy/upcloud/` is the UpCloud door
/// by name, so naming the roster row here is this crate's identity and not a
/// duplicated fact — everything that FOLLOWS from the name is read off the
/// roster below.
///
/// It is deliberately not read from gunnar's own `[[build.installer]]` rows:
/// none of them names a machine yet, and the reason is measured and recorded in
/// `crates/gunnar-recipe/tests/machine_named.rs` — naming `upcloud` there
/// derives `target.buses ∋ ide-cdrom`, which requires `CONFIG_ATA=y`, which the
/// `nuked` donor preset vetoes by name. The day that block lifts, this constant
/// becomes a lookup and that test says so.
pub const MACHINE: &str = "upcloud";

/// One face of one medium: the device kind it is attached as, the suffix its
/// storage title carries, and why it exists at all.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Face {
    pub kind: DeviceKind,
    /// The storage-title suffix (`<stem>-<sha8>-cdrom`).
    pub suffix: &'static str,
    /// What this face is FOR — printed in the step log, so an operator watching
    /// the run sees the reason and not just the call.
    pub role: &'static str,
}

/// **How a medium must be attached to a machine**, derived from the roster's
/// `MediumBring`. See the module header for why this is an enum with one arm.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Attach {
    /// The platform boots an ATTACHED ISO (`MediumBring::IsoCdrom`). It rides
    /// twice: as a cdrom, which is what the firmware boots, and as a virtio
    /// disk, which is the only bus `korp-installer` probes for its payload.
    CdromAndVirtio,
}

impl Attach {
    /// The faces, in attach order. The order is load-bearing: the cdrom goes on
    /// first because the boot order is set to `cdrom` and the firmware picks
    /// the first one it finds.
    pub fn faces(self) -> [Face; 2] {
        match self {
            Attach::CdromAndVirtio => [
                Face {
                    kind: DeviceKind::Cdrom,
                    suffix: "cdrom",
                    role: "what the firmware boots",
                },
                Face {
                    kind: DeviceKind::Disk,
                    suffix: "virtio",
                    role: "what korp-installer reads the payload from — it probes ONLY virtio",
                },
            ],
        }
    }
}

/// **The derivation.** `Ok` when the named machine takes an attached ISO; a
/// refusal sentence otherwise, naming the machine and how its medium really
/// arrives.
///
/// The `Err` is not an internal error — it is the message an operator reads,
/// and it has to be enough to act on. A platform that imports a disk image is
/// installed by producing that image (the composer writes it from the same
/// cell: `InstallerBuild::cloud_medium`), not by attaching an ISO to a running
/// server, and the refusal says so rather than leaving the reader to work out
/// which of the two doors they wanted.
pub fn attach_for(machine: &str) -> Result<Attach, String> {
    let Some(p) = presets::preset(machine) else {
        return Err(format!(
            "REFUSED: `{machine}` is not a machine in the roster — the declared ones are: {}",
            presets::names().join(", ")
        ));
    };
    if !p.medium.is_iso_attach() {
        return Err(format!(
            "REFUSED: machine `{machine}` ({}) takes its medium as {}. This verb attaches an \
             installer ISO to a running server, and the INSTALLER path does not exist on that \
             platform{}. Its install is a disk IMPORT: compose the cell that names this machine \
             and upload the container the composer writes beside the .iso.",
            p.cloud,
            p.medium.describe(),
            match p.gap() {
                Some(why) => format!(" — {why}"),
                None => String::new(),
            }
        ));
    }
    Ok(Attach::CdromAndVirtio)
}

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

    /// The derivation has to be measured against itself: one that answered the
    /// same for every input would pass every test above it and decide nothing.
    #[test]
    fn the_roster_really_splits_and_the_split_is_not_the_device_set() {
        let iso: Vec<_> = presets::PRESETS
            .iter()
            .filter(|p| p.medium.is_iso_attach())
            .map(|p| p.name)
            .collect();
        let imported: Vec<_> = presets::PRESETS
            .iter()
            .filter(|p| !p.medium.is_iso_attach())
            .map(|p| p.name)
            .collect();
        assert!(
            !iso.is_empty() && !imported.is_empty(),
            "{iso:?} / {imported:?}"
        );

        // The point of deriving from the NAME and never the shape: `upcloud`
        // and `scaleway-instance` present the same devices and split here.
        let up = presets::preset("upcloud").expect("roster");
        let scw = presets::preset("scaleway-instance").expect("roster");
        assert_eq!(up.boot_disk.as_str(), scw.boot_disk.as_str());
        assert_eq!(up.nic.as_str(), scw.nic.as_str());
        assert!(attach_for("upcloud").is_ok());
        assert!(attach_for("scaleway-instance").is_err());
    }
}