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

nordisk/edda

crates/appliance-grow/src/md.rs

//! **The volume half of a [`crate::World`]** — the data disks, over
//! korp-installer's `grow::runtime` (the SAME engine korp-start grows the set
//! with at boot). Feature `md`.
//!
//! Moved AS IS out of gunnar-server's `BoxWorld` (gunnar 18a44e3): everything
//! in it that touches the set and nothing that touches gunnar — the mount
//! reading, the unmount with its read-only guard tmpfs, the md stop, the PCI
//! rescan, the survey, G1–G6 + mount, the journal records and the spent-nonce
//! ledger beside the journal. A product's world holds one [`MdVolumes`] and
//! delegates its volume verbs to it; its gate, its stores and its twin stay
//! its own.
//!
//! What only a VM proves: every method here is a real syscall on a real block
//! device (umount2, md `array_state`, `/sys/bus/pci/rescan`, XFS_IOC_FSGROWFSDATA),
//! so the native tests drive the machine over a scripted world instead and
//! gunnar-growbench / the mock-upcloud functest drive THIS.

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

use korp_installer::grow::{self as engine, runtime};

use crate::world::{Grown, MemberFacts, SurveyFacts};
use crate::Step;

/// **The data set's volumes**, as one product's world holds them.
pub struct MdVolumes {
    /// The journal, opened at the first record and held for the growth's
    /// whole life (one writer per box, like `korp-start`'s).
    journal: Mutex<Option<engine::Journal>>,
    mountpoint: PathBuf,
}

impl std::fmt::Debug for MdVolumes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MdVolumes").field("mountpoint", &self.mountpoint).finish()
    }
}

impl Default for MdVolumes {
    fn default() -> Self {
        Self::new()
    }
}

impl MdVolumes {
    /// Over korp's own mountpoint (`/data`).
    pub fn new() -> Self {
        MdVolumes { journal: Mutex::new(None), mountpoint: runtime::data_mountpoint().to_path_buf() }
    }

    /// The mountpoint the set is served at.
    pub fn mountpoint(&self) -> &Path {
        &self.mountpoint
    }

    fn with_journal<T>(&self, f: impl FnOnce(&mut engine::Journal) -> Result<T, String>) -> Result<T, String> {
        let mut guard = self.journal.lock().unwrap_or_else(|p| p.into_inner());
        if guard.is_none() {
            let path = engine::journal_path();
            let j = engine::Journal::open(&path).map_err(|e| format!("open the grow journal {}: {e} — is /serialdat mounted?", path.display()))?;
            *guard = Some(j);
        }
        f(guard.as_mut().expect("opened above"))
    }

    fn log(line: &str) {
        tracing::info!(target: crate::TARGET, event = "grow.engine", "{line}");
    }

    /// `/serialdat/grow-journal`.
    pub fn journal_path(&self) -> PathBuf {
        engine::journal_path()
    }

    /// Is the set mounted at the mountpoint?
    pub fn is_mounted(&self) -> bool {
        runtime::is_mounted(&self.mountpoint)
    }

    /// `/data`'s size as mounted, 0 when not.
    pub fn mounted_bytes(&self) -> u64 {
        if !self.is_mounted() {
            return 0;
        }
        rustix::fs::statvfs(&self.mountpoint).map(|s| s.f_blocks * s.f_frsize).unwrap_or(0)
    }

    /// sync + `umount2` (never lazy; an EBUSY names the descriptors this
    /// process still holds), then a read-only tmpfs over the empty
    /// mountpoint: anything that still writes by path under /data during the
    /// window meets EROFS — an error it can report — instead of a file in the
    /// root filesystem's /data directory that the remount would hide.
    pub fn sync_and_unmount(&self) -> Result<(), String> {
        runtime::unmount(&self.mountpoint)?;
        nix::mount::mount(Some("none"), &self.mountpoint, Some("tmpfs"), nix::mount::MsFlags::MS_RDONLY, Some("size=64k"))
            .map_err(|e| format!("guard tmpfs over {}: {e}", self.mountpoint.display()))
    }

    /// md `array_state ← clear`, the stale node removed.
    pub fn stop_array(&self) -> Result<(), String> {
        runtime::stop_array(std::time::Duration::from_secs(10))
    }

    /// `/sys/bus/pci/rescan`.
    pub fn rescan(&self) -> Result<(), String> {
        runtime::pci_rescan()
    }

    /// The members as they are NOW (read-only; safe while the array runs or
    /// is stopped).
    pub fn survey(&self) -> SurveyFacts {
        let s = runtime::survey();
        SurveyFacts {
            verdict: s.verdict.clone(),
            raid_disks: s.raid_disks,
            eligible: s.eligible(),
            target_sectors: s.target_sectors(),
            set_uuid: s.set_uuid.clone(),
            description: s.describe(),
            members: s
                .members
                .iter()
                .map(|m| MemberFacts {
                    index: m.index,
                    device: m.disk.clone(),
                    partition: m.partition.clone(),
                    disk_bytes: m.disk_sectors * 512,
                    set_bytes: m.set_sectors * 512,
                    offered_bytes: m.device_offers * 512,
                    larger: m.larger,
                })
                .collect(),
        }
    }

    /// G1–G3 → assemble → G4 → mount → G5 → G6, with the serve-at-old-size
    /// fallback inside; `Err` only when nothing could be assembled or mounted.
    pub fn grow_and_mount(&self) -> Result<Grown, String> {
        // The guard tmpfs goes first; a mount over a mount would hide it and
        // leave it to pin nothing forever.
        if self.is_mounted() {
            nix::mount::umount2(&self.mountpoint, nix::mount::MntFlags::empty()).map_err(|e| format!("unguard {}: {e}", self.mountpoint.display()))?;
        }
        let mountpoint = self.mountpoint.clone();
        let mounted = self.with_journal(|j| runtime::grow_and_mount(j, &mountpoint, &mut Self::log))?;
        Ok(Grown {
            grew: mounted.grew,
            array_bytes: mounted.array_bytes,
            refusal: mounted.refusal,
            xfs: match &mounted.xfs {
                None => String::new(),
                Some(Ok((plan, after))) => format!("{plan:?} → {after} blocks"),
                Some(Err(e)) => format!("FAILED: {e}"),
            },
        })
    }

    /// The epoch a growth toward `target_sectors` belongs to.
    pub fn epoch_for(&self, target_sectors: u64) -> u64 {
        self.with_journal(|j| Ok(j.epoch_for(target_sectors))).unwrap_or(0)
    }

    /// One runtime record in the engine's own line format.
    pub fn note(&self, epoch: u64, target_sectors: u64, step: Step) -> Result<(), String> {
        let step = match step {
            Step::Drain => engine::Step::Drain,
            Step::Unmount => engine::Step::Unmount,
            Step::Stop => engine::Step::Stop,
            Step::Wait => engine::Step::Wait,
            Step::Rescan => engine::Step::Rescan,
            Step::Reopen => engine::Step::Reopen,
            Step::Abort => engine::Step::Abort,
            Step::Fail => engine::Step::Fail,
        };
        self.with_journal(|j| runtime::note(j, epoch, target_sectors, step).map(|_| ()))
    }

    /// The spent-nonce ledger beside the journal (off the stripe).
    pub fn remember_nonce(&self, nonce: &str) -> Result<bool, String> {
        let ledger = engine::journal_path().with_file_name(crate::world::NONCE_LEDGER_FILE);
        crate::world::remember_nonce_in(&ledger, nonce)
    }
}

impl crate::world::Volumes for MdVolumes {
    fn engine_present(&self) -> bool {
        true
    }
    fn journal_path(&self) -> PathBuf {
        MdVolumes::journal_path(self)
    }
    fn is_mounted(&self) -> bool {
        MdVolumes::is_mounted(self)
    }
    fn mounted_bytes(&self) -> u64 {
        MdVolumes::mounted_bytes(self)
    }
    fn sync_and_unmount(&self) -> Result<(), String> {
        MdVolumes::sync_and_unmount(self)
    }
    fn stop_array(&self) -> Result<(), String> {
        MdVolumes::stop_array(self)
    }
    fn rescan(&self) -> Result<(), String> {
        MdVolumes::rescan(self)
    }
    fn survey(&self) -> SurveyFacts {
        MdVolumes::survey(self)
    }
    fn grow_and_mount(&self) -> Result<Grown, String> {
        MdVolumes::grow_and_mount(self)
    }
    fn epoch_for(&self, target_sectors: u64) -> u64 {
        MdVolumes::epoch_for(self, target_sectors)
    }
    fn note(&self, epoch: u64, target_sectors: u64, step: Step) -> Result<(), String> {
        MdVolumes::note(self, epoch, target_sectors, step)
    }
    fn remember_nonce(&self, nonce: &str) -> Result<bool, String> {
        MdVolumes::remember_nonce(self, nonce)
    }
}