From b7a76d78f65e97251cb23822476a25ed3a091a1a Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jun 2026 12:38:21 +0300 Subject: [PATCH] =?UTF-8?q?feat(firstboot):=20=D0=B8=D0=B4=D0=B5=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D1=82=D0=B5=D0=BD=D1=82=D0=BD=D1=8B=D0=B9=20provis?= =?UTF-8?q?ion=20/data=20+=20machine-id=20(A06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Alexander --- Cargo.lock | 16 +++++ Cargo.toml | 7 +- crates/core/shturman-firstboot/Cargo.toml | 13 ++++ crates/core/shturman-firstboot/src/lib.rs | 81 ++++++++++++++++++++++ crates/core/shturman-firstboot/src/main.rs | 15 ++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 crates/core/shturman-firstboot/Cargo.toml create mode 100644 crates/core/shturman-firstboot/src/lib.rs create mode 100644 crates/core/shturman-firstboot/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index b12b4cc..08d82f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index be02a16..46ca484 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/core/shturman-firstboot/Cargo.toml b/crates/core/shturman-firstboot/Cargo.toml new file mode 100644 index 0000000..1db91f6 --- /dev/null +++ b/crates/core/shturman-firstboot/Cargo.toml @@ -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 diff --git a/crates/core/shturman-firstboot/src/lib.rs b/crates/core/shturman-firstboot/src/lib.rs new file mode 100644 index 0000000..4bb97ed --- /dev/null +++ b/crates/core/shturman-firstboot/src/lib.rs @@ -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 { + 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()); + } +} diff --git a/crates/core/shturman-firstboot/src/main.rs b/crates/core/shturman-firstboot/src/main.rs new file mode 100644 index 0000000..364a86e --- /dev/null +++ b/crates/core/shturman-firstboot/src/main.rs @@ -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(()) +}