//! First-boot provisioning (A06): идемпотентная подготовка `/data` + генерация `machine-id`. //! //! Привязка `machine-id` → `/etc/machine-id` — **отдельный every-boot юнит** (План 5), не здесь //! (bind волатилен, не переживает ребут). Дефолты настроек сеет сам `Settings` (single source формата). use shturman_common::{write_atomic, Layout}; use std::io::{self, Read}; /// Идемпотентно подготовить `/data`. `Ok(true)` — провизионинг выполнен; `Ok(false)` — уже было (no-op). /// Маркер пишется durable-write **последним**: частичный сбой mid-run → следующий прогон довосстановит. pub fn provision(layout: &Layout) -> io::Result { if layout.provisioned_marker().exists() { return Ok(false); } for dir in [ layout.apps(), layout.settings(), layout.state(), layout.log(), ] { std::fs::create_dir_all(&dir)?; } let mid = layout.state().join("machine-id"); if !mid.exists() { write_atomic(&mid, generate_machine_id()?.as_bytes())?; } write_atomic(&layout.provisioned_marker(), b"1\n")?; Ok(true) } /// 32 hex-символа из `/dev/urandom` (формат systemd `machine-id`). fn generate_machine_id() -> io::Result { let mut buf = [0u8; 16]; std::fs::File::open("/dev/urandom")?.read_exact(&mut buf)?; Ok(buf.iter().map(|b| format!("{b:02x}")).collect()) } #[cfg(test)] mod tests { use super::*; use shturman_common::Layout; #[test] fn provisions_then_idempotent() { let d = tempfile::tempdir().unwrap(); let l = Layout::new(d.path()); assert!(provision(&l).unwrap()); // первый запуск — провизионинг assert!(l.settings().is_dir()); assert!(l.state().join("machine-id").exists()); assert!(l.provisioned_marker().exists()); let mid1 = std::fs::read(l.state().join("machine-id")).unwrap(); assert!(!provision(&l).unwrap()); // второй — no-op let mid2 = std::fs::read(l.state().join("machine-id")).unwrap(); assert_eq!(mid1, mid2); // machine-id стабилен между прогонами } #[test] fn factory_reset_regenerates() { let d = tempfile::tempdir().unwrap(); let l = Layout::new(d.path()); provision(&l).unwrap(); let mid1 = std::fs::read(l.state().join("machine-id")).unwrap(); // factory-reset: очистка /data std::fs::remove_dir_all(d.path()).unwrap(); std::fs::create_dir_all(d.path()).unwrap(); assert!(provision(&l).unwrap()); let mid2 = std::fs::read(l.state().join("machine-id")).unwrap(); assert_ne!(mid1, mid2); // новый machine-id после wipe } #[test] fn recovers_when_marker_missing() { let d = tempfile::tempdir().unwrap(); let l = Layout::new(d.path()); // имитация прерывания mid-run: каталоги есть, маркера нет std::fs::create_dir_all(l.settings()).unwrap(); assert!(provision(&l).unwrap()); // довосстанавливает assert!(l.provisioned_marker().exists()); assert!(l.state().join("machine-id").exists()); } }