use std::io::{self, Write}; use std::os::fd::AsFd; use lazaret_core::records::{Domain, StateResetScopeV1}; use lazaret_domain_state::domain_store::HostCoordinatorDomainStore; use lazaret_domain_state::state_maintenance::{ DomainStateReport, StateAnchorEntryKind, inspect_absent_domain_state, inspect_domain_state, reset_domain_state, }; use lazaret_linux::filesystem::try_exclusive_flock; use rustix::fs::{Mode, OFlags, ResolveFlags, openat2}; use serde_json::json; use crate::ClientCommand; use crate::coordinator_backend::{load_configuration, open_private_domain_lock}; const OUTPUT_SCHEMA: &str = "lazaret-state-output-v1"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StateOperation { List, Inspect, Verify, Reset, } #[derive(Debug)] pub struct StateCommand { pub(crate) operation: StateOperation, pub(crate) context: ClientCommand, pub(crate) machine: bool, pub(crate) yes: bool, pub(crate) scope: Option, } /// Runs one state-maintenance command using resolved configuration and exclusive Domain authority. /// /// # Errors /// Refuses invalid arguments, active Domains, arbitrary targets, authority changes, malformed /// durable state, structural failures, and reset without confirmation. pub fn run_command(command: StateCommand) -> Result> { let configuration = load_configuration(&command.context)?; if let Some(StateResetScopeV1::Selected(path)) = &command.scope && !configuration .fingerprint_record() .persistent_dirs .iter() .any(|configured| configured.as_bytes() == path) { return Err("reset target is not an exact configured persistent container path".into()); } let root = configuration.persistent_domain_root().open_authority()?; let lock = match try_exclusive_flock(open_private_domain_lock(&root)?) { Ok(lock) => lock, Err(error) if error.kind() == io::ErrorKind::WouldBlock => { return Err("Domain state is busy; exit active Lazaret sessions or wait for another maintenance command to finish, then retry".into()); } Err(error) => return Err(error.into()), }; let database = match openat2( root.as_fd(), "domain.redb", OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_XDEV, ) { Ok(database) => database, Err(rustix::io::Errno::NOENT) => { let report = inspect_absent_domain_state(&configuration)?; write_report(&report, command.operation, command.machine); return Ok(0); } Err(error) => return Err(io::Error::from(error).into()), }; let expected = Domain { domain_id: configuration.domain_id(), profile_name: configuration.selected_profile().to_owned(), canonical_workspace: configuration.workspace().canonical_label().clone(), }; let store = HostCoordinatorDomainStore::open_file(lock, database.into(), &expected)?; let report = match command.operation { StateOperation::List | StateOperation::Inspect | StateOperation::Verify => { inspect_domain_state(&configuration, &store)? } StateOperation::Reset => { let scope = command.scope.ok_or("reset scope is missing")?; if !command.yes { confirm(&configuration, &scope)?; } reset_domain_state(&configuration, &store, scope)? } }; write_report(&report, command.operation, command.machine); Ok(0) } fn confirm( configuration: &lazaret_core::configuration::ResolvedDomainConfiguration, scope: &StateResetScopeV1, ) -> Result<(), Box> { let scope = match scope { StateResetScopeV1::All => "all configured State Anchors".to_owned(), StateResetScopeV1::Selected(path) => { format!("State Anchor {}", String::from_utf8_lossy(path)) } }; eprintln!( "Reset Domain {} profile {} workspace {} scope: {}? [y/N]", hex(configuration.domain_id().as_bytes()), configuration.selected_profile(), String::from_utf8_lossy(configuration.workspace().canonical_label().as_bytes()), scope ); io::stderr().flush()?; let mut answer = String::new(); io::stdin().read_line(&mut answer)?; if matches!(answer.trim(), "y" | "Y" | "yes" | "YES") { Ok(()) } else { Err("reset was not confirmed".into()) } } fn write_report(report: &DomainStateReport, operation: StateOperation, machine: bool) { if machine { let anchors = report .anchors .iter() .map(|anchor| { json!({ "container_path": anchor.container_path, "changes": anchor.changes.iter().map(|entry| json!({ "path_hex": hex(&entry.path), "kind": entry_kind(entry.kind), "mode": entry.mode, "uid": entry.uid, "gid": entry.gid, "size": entry.size, })).collect::>(), }) }) .collect::>(); println!( "{}", json!({ "schema": OUTPUT_SCHEMA, "operation": operation_name(operation), "domain_id": hex(&report.domain_id), "profile": report.profile, "workspace_hex": hex(&report.workspace), "anchors": anchors, "contents_included": false, }) ); } else { println!("Domain {}", hex(&report.domain_id)); println!("Profile: {}", report.profile); println!("Workspace: {}", String::from_utf8_lossy(&report.workspace)); for anchor in &report.anchors { println!( "State Anchor {}: {} structural change(s)", anchor.container_path, anchor.changes.len() ); if matches!(operation, StateOperation::Inspect | StateOperation::Verify) { for entry in &anchor.changes { println!( " {} {} mode {:o} uid {} gid {} size {}", String::from_utf8_lossy(&entry.path), entry_kind(entry.kind), entry.mode, entry.uid, entry.gid, entry.size ); } } } println!("Persisted file contents were not read or printed."); } } const fn operation_name(operation: StateOperation) -> &'static str { match operation { StateOperation::List => "list", StateOperation::Inspect => "inspect", StateOperation::Verify => "verify", StateOperation::Reset => "reset", } } const fn entry_kind(kind: StateAnchorEntryKind) -> &'static str { match kind { StateAnchorEntryKind::Directory => "directory", StateAnchorEntryKind::RegularFile => "regular-file", StateAnchorEntryKind::Symlink => "symlink", StateAnchorEntryKind::Whiteout => "whiteout", } } fn hex(bytes: &[u8]) -> String { use std::fmt::Write as _; bytes.iter().fold(String::new(), |mut output, byte| { write!(output, "{byte:02x}").expect("writing to a String cannot fail"); output }) }