Files
shturman/crates/core/shturman-firstboot/src/lib.rs
T
kk0t9 b7a76d78f6 feat(firstboot): идемпотентный provision /data + machine-id (A06)
lib provision (idempotent; factory-reset; recover mid-run) + bin; machine-id из /dev/urandom.
Привязка к /etc/machine-id — every-boot юнит (План 5). Дефолты настроек — Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
2026-06-24 12:38:21 +03:00

82 lines
3.4 KiB
Rust

//! 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<bool> {
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<String> {
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());
}
}