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

nordisk/Skidbladnir

src/platform.rs

//! AIRGAP3 (target half) — per-OS specialization + unpack.
//!
//! Design (`.nornir/airgap-install.md` §2 Phase 1): per-platform prep (e.g.
//! optionally disable SELinux on RHEL), then unpack the verified bundle into the
//! software dir. The SELinux toggle is a real (annotated) deploy subprocess; the
//! unpack reuses [`crate::archive`]. Detection is from `/etc/os-release` (no
//! network).

use std::path::Path;

use anyhow::{Context, Result};

/// Target platform family for specialization.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
    Rhel,
    Debian,
    Other,
}

impl Platform {
    /// Detect the platform from `/etc/os-release` (offline; no network).
    pub fn detect() -> Platform {
        let os = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
        Platform::from_os_release(&os)
    }

    /// Parse a platform family from `/etc/os-release` contents (pure → testable).
    pub fn from_os_release(contents: &str) -> Platform {
        let lc = contents.to_lowercase();
        // ID_LIKE / ID covers the common families.
        if lc.contains("rhel")
            || lc.contains("centos")
            || lc.contains("fedora")
            || lc.contains("rocky")
            || lc.contains("almalinux")
        {
            Platform::Rhel
        } else if lc.contains("debian") || lc.contains("ubuntu") {
            Platform::Debian
        } else {
            Platform::Other
        }
    }

    /// Whether this family uses SELinux (the toggle only applies to RHEL-likes).
    pub fn uses_selinux(self) -> bool {
        matches!(self, Platform::Rhel)
    }
}

/// Apply per-platform prep. On RHEL-likes, optionally set SELinux to permissive
/// (`setenforce 0`) when `disable_selinux` is set — an annotated deploy
/// subprocess, idempotent and non-fatal if the tool is absent. Other families
/// are a no-op. Returns the actions taken (for telemetry / tests).
pub fn specialize(p: Platform, disable_selinux: bool) -> Result<Vec<String>> {
    let mut actions = Vec::new();
    if p.uses_selinux() && disable_selinux {
        // 100% Rust (no `setenforce` subprocess): `setenforce 0` is exactly
        // `echo 0 > /sys/fs/selinux/enforce`. Non-fatal — a minimal airgap image
        // may not mount selinuxfs.
        let ran = std::fs::write("/sys/fs/selinux/enforce", b"0").is_ok();
        actions.push(if ran {
            "selinux:permissive".to_string()
        } else {
            "selinux:skip(no setenforce)".to_string()
        });
    }
    Ok(actions)
}

/// Unpack the (already-verified) bundle into the software dir. Thin wrapper over
/// [`crate::archive::Archive::extract_all`] so platform prep + unpack read as one
/// Phase 1 step.
pub fn unpack(
    archive: &dyn crate::archive::Archive,
    bundle: &Path,
    software_dir: &Path,
) -> Result<()> {
    archive
        .extract_all(bundle, software_dir)
        .with_context(|| format!("unpack bundle into {}", software_dir.display()))
}

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

    /// LAW 1 (inject-assert): os-release parsing maps real distro strings to the
    /// right family, and only RHEL-likes report SELinux.
    #[test]
    fn os_release_detects_family_and_selinux() {
        let rhel = "ID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"9.3\"\n";
        assert_eq!(Platform::from_os_release(rhel), Platform::Rhel);
        assert!(Platform::from_os_release(rhel).uses_selinux());

        let ubuntu = "ID=ubuntu\nID_LIKE=debian\n";
        assert_eq!(Platform::from_os_release(ubuntu), Platform::Debian);
        assert!(!Platform::from_os_release(ubuntu).uses_selinux());

        assert_eq!(Platform::from_os_release("ID=alpine\n"), Platform::Other);
    }

    /// `specialize` is a no-op (no actions) on non-SELinux platforms, and on RHEL
    /// it records a selinux action (the tool itself is non-fatal in test envs).
    #[test]
    fn specialize_only_touches_selinux_on_rhel() {
        assert!(specialize(Platform::Debian, true).unwrap().is_empty());
        assert!(specialize(Platform::Rhel, false).unwrap().is_empty());
        let acts = specialize(Platform::Rhel, true).unwrap();
        assert_eq!(acts.len(), 1);
        assert!(acts[0].starts_with("selinux:"));
    }

    /// LAW 1: unpack actually extracts the bundle tree into the software dir.
    #[test]
    fn unpack_extracts_bundle_into_software_dir() {
        use crate::archive::{Archive, TarZstd};
        use std::io::Write as _;
        let dir = tempfile::tempdir().unwrap();
        let staging = dir.path().join("stage");
        std::fs::create_dir_all(staging.join("linux-x86_64")).unwrap();
        std::fs::File::create(staging.join("linux-x86_64/holger"))
            .unwrap()
            .write_all(b"bin")
            .unwrap();
        let bundle = dir.path().join("b.tar.zst");
        let eng = TarZstd::new();
        eng.pack(&staging, "v", &bundle).unwrap();

        let sw = dir.path().join("software");
        unpack(&eng, &bundle, &sw).unwrap();
        assert_eq!(
            std::fs::read(sw.join("linux-x86_64/holger")).unwrap(),
            b"bin"
        );
    }
}