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>
This commit is contained in:
Generated
+16
@@ -11,6 +11,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -764,6 +770,16 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shturman-firstboot"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"shturman-common",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shturman-ipc"
|
||||
version = "0.0.0"
|
||||
|
||||
+6
-1
@@ -2,7 +2,12 @@
|
||||
resolver = "2"
|
||||
# Члены растут по планам реализации. crates/core, crates/apps, crates/tools —
|
||||
# группировка привилегированного ядра / first-party-апов / dev-инструментов (architecture §3).
|
||||
members = ["crates/shturman-common", "crates/shturman-ipc", "crates/shturman-sdk"]
|
||||
members = [
|
||||
"crates/shturman-common",
|
||||
"crates/shturman-ipc",
|
||||
"crates/shturman-sdk",
|
||||
"crates/core/shturman-firstboot",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "shturman-firstboot"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
shturman-common = { path = "../../shturman-common" }
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
@@ -0,0 +1,81 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! First-boot provisioning (A06) — oneshot. Гейт `ConditionPathExists=!/data/.shturman-provisioned`
|
||||
//! и порядок (After data.mount, до сервисов) задаёт systemd (План 5).
|
||||
|
||||
use shturman_common::{init_tracing, Layout};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
init_tracing("shturman-firstboot");
|
||||
let layout = Layout::from_env();
|
||||
if shturman_firstboot::provision(&layout)? {
|
||||
tracing::info!(root = %layout.root().display(), "first-boot: provisioning выполнен");
|
||||
} else {
|
||||
tracing::info!("first-boot: уже провижено — no-op");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user