// REGION: API — one-liners for plan_mounts/apply_mounts/maybe_write_fstab and structs // api: mount::MountPlan { root_mounts: Vec, subvol_mounts: Vec, primary_uuid: Option } // api: mount::MountResult { source: String, target: String, fstype: String, options: String } // api: mount::plan_mounts(fs_results: &[crate::fs::FsResult], cfg: &crate::types::Config) -> crate::Result // api: mount::apply_mounts(plan: &MountPlan) -> crate::Result> // api: mount::maybe_write_fstab(mounts: &[MountResult], cfg: &crate::types::Config) -> crate::Result<()> // REGION: API-END // // REGION: RESPONSIBILITIES // - Implement mount phase only: plan root mounts under /var/mounts/{UUID}, ensure/plan subvols, and mount subvols to /var/cache/*. // - Use UUID= sources, deterministic primary selection (first FsResult) for dual_independent. // - Generate fstab entries only for four subvol targets; exclude runtime root mounts. // REGION: RESPONSIBILITIES-END // // REGION: SAFETY // - Never mount ESP; only Btrfs/Bcachefs data FS. Root btrfs mounts use subvolid=5 (top-level). // - Create-if-missing subvolumes prior to subvol mounts; ensure directories exist. // - Always use UUID= sources; no device paths. // - Bcachefs subvolume mounts use option key 'X-mount.subdir={name}' (not 'subvol='). // REGION: SAFETY-END // // REGION: ERROR_MAPPING // - External tool failures map to Error::Tool via util::run_cmd/run_cmd_capture. // - Missing required tools map to Error::Mount with clear explanation. // REGION: ERROR_MAPPING-END // // REGION: TODO // - Defer compression/SSD options; later map from Config into mount options. // - Consider validating tool presence up-front for clearer early errors. // REGION: TODO-END //! Mount planning and application. //! //! See [fn plan_mounts()](src/mount/ops.rs:1), [fn apply_mounts()](src/mount/ops.rs:1), //! and [fn maybe_write_fstab()](src/mount/ops.rs:1). #![allow(dead_code)] use crate::{ fs::{FsKind, FsResult}, types::Config, util::{run_cmd, run_cmd_capture, which_tool}, Error, Result, }; use std::collections::HashMap; use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::Path; use tracing::info; const ROOT_BASE: &str = "/var/mounts"; const TARGET_SYSTEM: &str = "/var/cache/system"; const TARGET_ETC: &str = "/var/cache/etc"; const TARGET_MODULES: &str = "/var/cache/modules"; const TARGET_VM_META: &str = "/var/cache/vm-meta"; const SUBVOLS: &[&str] = &["system", "etc", "modules", "vm-meta"]; #[derive(Debug, Clone)] struct ExistingMount { source: String, fstype: String, options: String, } fn current_mounts() -> HashMap { let mut map = HashMap::new(); if let Ok(content) = std::fs::read_to_string("/proc/self/mountinfo") { for line in content.lines() { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 7 { continue; } let target = parts[4].to_string(); let mount_options = parts[5].to_string(); if let Some(idx) = parts.iter().position(|p| *p == "-") { if idx + 2 < parts.len() { let fstype = parts[idx + 1].to_string(); let source = parts[idx + 2].to_string(); let super_opts = if idx + 3 < parts.len() { parts[idx + 3].to_string() } else { String::new() }; let combined_options = if super_opts.is_empty() { mount_options.clone() } else { format!("{mount_options},{super_opts}") }; map.insert( target, ExistingMount { source, fstype, options: combined_options, }, ); } } } } map } fn source_matches_uuid(existing_source: &str, uuid: &str) -> bool { if existing_source == format!("UUID={}", uuid) { return true; } if let Some(existing_uuid) = existing_source.strip_prefix("UUID=") { return existing_uuid == uuid; } if existing_source.starts_with("/dev/") { let uuid_path = Path::new("/dev/disk/by-uuid").join(uuid); if let (Ok(existing_canon), Ok(uuid_canon)) = ( std::fs::canonicalize(existing_source), std::fs::canonicalize(&uuid_path), ) { return existing_canon == uuid_canon; } } false } #[derive(Debug, Clone)] pub struct PlannedMount { pub uuid: String, // UUID string without prefix pub target: String, // absolute path pub fstype: String, // "btrfs" | "bcachefs" pub options: String, // e.g., "rw,noatime,subvolid=5" } #[derive(Debug, Clone)] pub struct PlannedSubvolMount { pub uuid: String, // UUID of primary FS pub name: String, // subvol name (system/etc/modules/vm-meta) pub target: String, // absolute final target pub fstype: String, // "btrfs" | "bcachefs" pub options: String, // e.g., "rw,noatime,subvol=system" } /// Mount plan per policy. #[derive(Debug, Clone)] pub struct MountPlan { /// Root mounts under /var/mounts/{UUID} for all data filesystems. pub root_mounts: Vec, /// Four subvol mounts chosen from the primary FS only. pub subvol_mounts: Vec, /// Primary UUID selection (only data FS; for multiple pick first in input order). pub primary_uuid: Option, } /// Result of applying a mount (root or subvol). #[derive(Debug, Clone)] pub struct MountResult { /// Source as "UUID=..." (never device paths). pub source: String, /// Target directory. pub target: String, /// Filesystem type string. pub fstype: String, /// Options used for the mount. pub options: String, } fn fstype_str(kind: FsKind) -> &'static str { match kind { FsKind::Btrfs => "btrfs", FsKind::Bcachefs => "bcachefs", FsKind::Vfat => "vfat", } } /// Build mount plan per policy. pub fn plan_mounts(fs_results: &[FsResult], _cfg: &Config) -> Result { // Identify data filesystems (Btrfs/Bcachefs), ignore ESP (Vfat) let data: Vec<&FsResult> = fs_results .iter() .filter(|r| matches!(r.kind, FsKind::Btrfs | FsKind::Bcachefs)) .collect(); if data.is_empty() { return Err(Error::Mount( "no data filesystems to mount (expected Btrfs or Bcachefs)".into(), )); } // Root mounts for all data filesystems let mut root_mounts: Vec = Vec::new(); for r in &data { let uuid = r.uuid.clone(); let fstype = fstype_str(r.kind).to_string(); let target = format!("{}/{}", ROOT_BASE, uuid); let options = match r.kind { FsKind::Btrfs => "rw,noatime,subvolid=5".to_string(), FsKind::Bcachefs => "rw,noatime".to_string(), FsKind::Vfat => continue, }; root_mounts.push(PlannedMount { uuid, target, fstype, options, }); } // Determine primary UUID let primary_uuid = Some(data[0].uuid.clone()); // Subvol mounts only from primary FS let primary = data[0]; let mut subvol_mounts: Vec = Vec::new(); let fstype = fstype_str(primary.kind).to_string(); // Option key differs per filesystem: btrfs uses subvol=, bcachefs uses X-mount.subdir= let opt_key = match primary.kind { FsKind::Btrfs => "subvol=", FsKind::Bcachefs => "X-mount.subdir=", FsKind::Vfat => "subvol=", // not used for Vfat (ESP ignored) }; for name in SUBVOLS { let target = match *name { "system" => TARGET_SYSTEM.to_string(), "etc" => TARGET_ETC.to_string(), "modules" => TARGET_MODULES.to_string(), "vm-meta" => TARGET_VM_META.to_string(), _ => continue, }; let options = format!("rw,noatime,{}{}", opt_key, name); subvol_mounts.push(PlannedSubvolMount { uuid: primary.uuid.clone(), name: name.to_string(), target, fstype: fstype.clone(), options, }); } Ok(MountPlan { root_mounts, subvol_mounts, primary_uuid, }) } /// Apply mounts: ensure dirs, mount roots, create subvols if missing, mount subvols. pub fn apply_mounts(plan: &MountPlan) -> Result> { // Tool discovery let mount_tool = which_tool("mount")? .ok_or_else(|| Error::Mount("required tool 'mount' not found in PATH".into()))?; // Ensure target directories exist for root mounts for pm in &plan.root_mounts { create_dir_all(&pm.target) .map_err(|e| Error::Mount(format!("failed to create dir {}: {}", pm.target, e)))?; } // Ensure final subvol targets exist for sm in &plan.subvol_mounts { create_dir_all(&sm.target) .map_err(|e| Error::Mount(format!("failed to create dir {}: {}", sm.target, e)))?; } let mut results_map: HashMap = HashMap::new(); let mut existing_mounts = current_mounts(); // Root mounts for pm in &plan.root_mounts { let source = format!("UUID={}", pm.uuid); if let Some(existing) = existing_mounts.get(pm.target.as_str()) { if source_matches_uuid(&existing.source, &pm.uuid) { info!( "mount::apply_mounts: target {} already mounted; skipping", pm.target ); let existing_fstype = existing.fstype.clone(); let existing_options = existing.options.clone(); results_map .entry(pm.target.clone()) .or_insert_with(|| MountResult { source: source.clone(), target: pm.target.clone(), fstype: existing_fstype, options: existing_options, }); continue; } else { return Err(Error::Mount(format!( "target {} already mounted by {} (expected UUID={})", pm.target, existing.source, pm.uuid ))); } } let args = [ mount_tool.as_str(), "-t", pm.fstype.as_str(), "-o", pm.options.as_str(), source.as_str(), pm.target.as_str(), ]; run_cmd(&args)?; existing_mounts.insert( pm.target.clone(), ExistingMount { source: source.clone(), fstype: pm.fstype.clone(), options: pm.options.clone(), }, ); results_map.insert( pm.target.clone(), MountResult { source, target: pm.target.clone(), fstype: pm.fstype.clone(), options: pm.options.clone(), }, ); } // Subvolume creation (create-if-missing) and mounts for the primary if let Some(primary_uuid) = &plan.primary_uuid { // Determine primary fs kind from planned subvols (they all share fstype for primary) let primary_kind = plan .subvol_mounts .get(0) .map(|s| s.fstype.clone()) .unwrap_or_else(|| "btrfs".to_string()); let root = format!("{}/{}", ROOT_BASE, primary_uuid); if primary_kind == "btrfs" { let btrfs_tool = which_tool("btrfs")? .ok_or_else(|| Error::Mount("required tool 'btrfs' not found in PATH".into()))?; // List existing subvols under root let out = run_cmd_capture(&[ btrfs_tool.as_str(), "subvolume", "list", "-o", root.as_str(), ])?; for sm in &plan.subvol_mounts { if &sm.uuid != primary_uuid { continue; } // Check existence by scanning output for " path {name}" let exists = out .stdout .lines() .any(|l| l.contains(&format!(" path {}", sm.name))); if !exists { // Create subvolume let subvol_path = format!("{}/{}", root, sm.name); let args = [btrfs_tool.as_str(), "subvolume", "create", subvol_path.as_str()]; run_cmd(&args)?; } } } else if primary_kind == "bcachefs" { let bcachefs_tool = which_tool("bcachefs")?.ok_or_else(|| { Error::Mount("required tool 'bcachefs' not found in PATH".into()) })?; for sm in &plan.subvol_mounts { if &sm.uuid != primary_uuid { continue; } let subvol_path = format!("{}/{}", root, sm.name); if !Path::new(&subvol_path).exists() { let args = [ bcachefs_tool.as_str(), "subvolume", "create", subvol_path.as_str(), ]; run_cmd(&args)?; } } } else { return Err(Error::Mount(format!( "unsupported primary fstype for subvols: {}", primary_kind ))); } } // Subvol mounts for sm in &plan.subvol_mounts { let source = format!("UUID={}", sm.uuid); if let Some(existing) = existing_mounts.get(sm.target.as_str()) { if source_matches_uuid(&existing.source, &sm.uuid) { info!( "mount::apply_mounts: target {} already mounted; skipping", sm.target ); let existing_fstype = existing.fstype.clone(); let existing_options = existing.options.clone(); results_map .entry(sm.target.clone()) .or_insert_with(|| MountResult { source: source.clone(), target: sm.target.clone(), fstype: existing_fstype, options: existing_options, }); continue; } else { return Err(Error::Mount(format!( "target {} already mounted by {} (expected UUID={})", sm.target, existing.source, sm.uuid ))); } } let args = [ mount_tool.as_str(), "-t", sm.fstype.as_str(), "-o", sm.options.as_str(), source.as_str(), sm.target.as_str(), ]; run_cmd(&args)?; existing_mounts.insert( sm.target.clone(), ExistingMount { source: source.clone(), fstype: sm.fstype.clone(), options: sm.options.clone(), }, ); results_map.insert( sm.target.clone(), MountResult { source, target: sm.target.clone(), fstype: sm.fstype.clone(), options: sm.options.clone(), }, ); } let mut results: Vec = results_map.into_values().collect(); results.sort_by(|a, b| a.target.cmp(&b.target)); Ok(results) } /// Optionally write fstab entries for subvol mounts only (deterministic order). pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> { if !cfg.mount.fstab_enabled { return Ok(()); } // Partition mount results into runtime root mounts and final subvolume targets. let mut root_entries: Vec<&MountResult> = mounts .iter() .filter(|m| m.target.starts_with(ROOT_BASE)) .collect(); let wanted = [TARGET_ETC, TARGET_MODULES, TARGET_SYSTEM, TARGET_VM_META]; let mut subvol_entries: Vec<&MountResult> = mounts .iter() .filter(|m| wanted.contains(&m.target.as_str())) .collect(); // Sort by target path ascending to be deterministic (roots before subvols). root_entries.sort_by(|a, b| a.target.cmp(&b.target)); subvol_entries.sort_by(|a, b| a.target.cmp(&b.target)); // Compose lines: include all root mounts first, followed by the four subvol targets. let mut lines: Vec = Vec::new(); for m in root_entries.into_iter().chain(subvol_entries.into_iter()) { // m.source already "UUID=..." let line = format!( "{} {} {} {} 0 0", m.source, m.target, m.fstype, m.options ); lines.push(line); } // Atomic write to /etc/fstab let fstab_path = "/etc/fstab"; let tmp_path = "/etc/fstab.zosstorage.tmp"; if let Some(parent) = Path::new(fstab_path).parent() { create_dir_all(parent) .map_err(|e| Error::Mount(format!("failed to create {}: {}", parent.display(), e)))?; } { let mut f = File::create(tmp_path) .map_err(|e| Error::Mount(format!("failed to create {}: {}", tmp_path, e)))?; for line in lines { writeln!(f, "{}", line) .map_err(|e| Error::Mount(format!("failed to write tmp fstab: {}", e)))?; } f.flush() .map_err(|e| Error::Mount(format!("failed to flush tmp fstab: {}", e)))?; } std::fs::rename(tmp_path, fstab_path).map_err(|e| { Error::Mount(format!( "failed to replace {} atomically: {}", fstab_path, e )) })?; Ok(()) }