// REGION: API // api: fs::FsKind { Vfat, Btrfs, Bcachefs } // api: fs::FsSpec { kind: FsKind, devices: Vec, label: String } // api: fs::FsPlan { specs: Vec } // api: fs::FsResult { kind: FsKind, devices: Vec, uuid: String, label: String } // api: fs::plan_filesystems(parts: &[crate::partition::PartitionResult], cfg: &crate::config::types::Config) -> crate::Result // api: fs::make_filesystems(plan: &FsPlan) -> crate::Result> // REGION: API-END // // REGION: RESPONSIBILITIES // - Map partition roles to concrete filesystems (vfat for ESP, btrfs for data, bcachefs for SSD+HDD). // - Execute mkfs operations via external tooling wrappers and capture resulting UUIDs/labels. // Non-goals: partition layout decisions, mount orchestration, device discovery. // REGION: RESPONSIBILITIES-END // // REGION: EXTENSION_POINTS // ext: support additional filesystems or tuning flags through Config (e.g., more btrfs/bcachefs options). // ext: dry-run mode to emit mkfs commands without executing (future). // REGION: EXTENSION_POINTS-END // // REGION: SAFETY // safety: must not run mkfs on non-empty or unexpected partitions; assume prior validation enforced. // safety: ensure labels follow reserved semantics (ZOSBOOT for ESP, ZOSDATA for all data FS). // safety: mkfs.btrfs uses -f in apply path immediately after partitioning to handle leftover signatures. // REGION: SAFETY-END // // REGION: ERROR_MAPPING // errmap: external mkfs/blkid failures -> crate::Error::Tool with captured stderr. // errmap: planning mismatches -> crate::Error::Filesystem with context. // REGION: ERROR_MAPPING-END // // REGION: TODO // todo: bcachefs tuning flags mapping from config (compression/checksum/cache_mode) deferred // todo: add UUID consistency checks across multi-device filesystems // REGION: TODO-END //! Filesystem planning and creation for zosstorage. //! //! Maps partition results to concrete filesystems (vfat, btrfs, bcachefs) //! and executes mkfs operations via external tooling wrappers. //! //! See [fn plan_filesystems](plan.rs:1) and //! [fn make_filesystems](plan.rs:1). use crate::{ Result, types::{Config, Topology}, partition::{PartitionResult, PartRole}, util::{run_cmd, run_cmd_capture, which_tool}, Error, }; use tracing::{debug, warn}; /// Filesystem kinds supported by zosstorage. #[derive(Debug, Clone, Copy)] pub enum FsKind { /// FAT32 for the EFI System Partition. Vfat, /// Btrfs for data filesystems in single/dual topologies. Btrfs, /// Bcachefs for SSD+HDD topology (SSD as cache/promote, HDD backing). Bcachefs, } /// Declarative specification for creating a filesystem. #[derive(Debug, Clone)] pub struct FsSpec { /// Filesystem kind. pub kind: FsKind, /// Source device(s): /// - single path for vfat and btrfs /// - two paths for bcachefs (cache + backing) pub devices: Vec, /// Filesystem label: /// - "ZOSBOOT" for ESP /// - "ZOSDATA" for all data filesystems pub label: String, } /// Plan of filesystem creations. #[derive(Debug, Clone)] pub struct FsPlan { /// All filesystem creation specs. pub specs: Vec, } /// Result of creating a filesystem. #[derive(Debug, Clone)] pub struct FsResult { /// Filesystem kind. pub kind: FsKind, /// Devices the filesystem was created on. pub devices: Vec, /// Filesystem UUID (string as reported by blkid or related). pub uuid: String, /// Filesystem label ("ZOSBOOT" or "ZOSDATA"). pub label: String, } /// Determine which partitions get which filesystem based on topology. /// /// Rules: /// - ESP partitions => Vfat with label from cfg.filesystem.vfat.label (reserved "ZOSBOOT") /// - Data partitions => Btrfs with label cfg.filesystem.btrfs.label ("ZOSDATA"), unless topology SsdHddBcachefs /// - SsdHddBcachefs => pair one Cache partition (SSD) with one Data partition (HDD) into one Bcachefs FsSpec with devices [cache, data] and label cfg.filesystem.bcachefs.label ("ZOSDATA") /// - DualIndependent/BtrfsRaid1 => map each Data partition to its own Btrfs FsSpec (raid profile concerns are handled later during mkfs) pub fn plan_filesystems( parts: &[PartitionResult], cfg: &Config, ) -> Result { let mut specs: Vec = Vec::new(); // Always map ESP partitions for p in parts.iter().filter(|p| matches!(p.role, PartRole::Esp)) { specs.push(FsSpec { kind: FsKind::Vfat, devices: vec![p.device_path.clone()], label: cfg.filesystem.vfat.label.clone(), }); } match cfg.topology { Topology::SsdHddBcachefs => { // Expect exactly one cache (SSD) and at least one data (HDD). Use the first data for pairing. let cache = parts.iter().find(|p| matches!(p.role, PartRole::Cache)) .ok_or_else(|| Error::Filesystem("expected a Cache partition for SsdHddBcachefs topology".to_string()))?; let data = parts.iter().find(|p| matches!(p.role, PartRole::Data)) .ok_or_else(|| Error::Filesystem("expected a Data partition for SsdHddBcachefs topology".to_string()))?; specs.push(FsSpec { kind: FsKind::Bcachefs, devices: vec![cache.device_path.clone(), data.device_path.clone()], label: cfg.filesystem.bcachefs.label.clone(), }); } Topology::BtrfsRaid1 => { // Group all Data partitions into a single Btrfs filesystem across multiple devices. let data_devs: Vec = parts .iter() .filter(|p| matches!(p.role, PartRole::Data)) .map(|p| p.device_path.clone()) .collect(); if data_devs.len() < 2 { return Err(Error::Filesystem( "BtrfsRaid1 topology requires at least 2 data partitions".to_string(), )); } specs.push(FsSpec { kind: FsKind::Btrfs, devices: data_devs, label: cfg.filesystem.btrfs.label.clone(), }); } _ => { // Map each Data partition to individual Btrfs filesystems. for p in parts.iter().filter(|p| matches!(p.role, PartRole::Data)) { specs.push(FsSpec { kind: FsKind::Btrfs, devices: vec![p.device_path.clone()], label: cfg.filesystem.btrfs.label.clone(), }); } } } if specs.is_empty() { return Err(Error::Filesystem("no filesystems to create from provided partitions".to_string())); } Ok(FsPlan { specs }) } /// Create the filesystems and return identity info (UUIDs, labels). /// //// Uses external tooling via util wrappers (mkfs.vfat, mkfs.btrfs, bcachefs format). /// Notes: /// - This initial implementation applies labels and creates filesystems with minimal flags. /// - Btrfs RAID profile (e.g., raid1) will be applied in a follow-up by mapping config to mkfs flags. /// - UUID is captured via blkid -o export on the first device of each spec. pub fn make_filesystems(plan: &FsPlan) -> Result> { // Discover required tools up-front let vfat_tool = which_tool("mkfs.vfat")?; let btrfs_tool = which_tool("mkfs.btrfs")?; let bcachefs_tool = which_tool("bcachefs")?; let blkid_tool = which_tool("blkid")?; if blkid_tool.is_none() { return Err(Error::Filesystem("blkid not found in PATH; cannot capture filesystem UUIDs".into())); } let blkid = blkid_tool.unwrap(); let mut results: Vec = Vec::new(); for spec in &plan.specs { match spec.kind { FsKind::Vfat => { let Some(ref mkfs) = vfat_tool else { return Err(Error::Filesystem("mkfs.vfat not found in PATH".into())); }; if spec.devices.len() != 1 { return Err(Error::Filesystem("vfat requires exactly one device".into())); } let dev = &spec.devices[0]; // mkfs.vfat -n LABEL /dev/... run_cmd(&[mkfs.as_str(), "-n", spec.label.as_str(), dev.as_str()])?; // Capture UUID let uuid = capture_uuid(&blkid, dev)?; results.push(FsResult { kind: FsKind::Vfat, devices: vec![dev.clone()], uuid, label: spec.label.clone(), }); } FsKind::Btrfs => { let Some(ref mkfs) = btrfs_tool else { return Err(Error::Filesystem("mkfs.btrfs not found in PATH".into())); }; if spec.devices.is_empty() { return Err(Error::Filesystem("btrfs requires at least one device".into())); } // mkfs.btrfs -L LABEL [ -m raid1 -d raid1 (when multi-device/raid1) ] dev1 [dev2 ...] let mut args: Vec = vec![mkfs.clone(), "-L".into(), spec.label.clone()]; // If this Btrfs is multi-device (as planned in BtrfsRaid1 topology), // set metadata/data profiles to raid1. This keeps plan/apply consistent. if spec.devices.len() >= 2 { args.push("-m".into()); args.push("raid1".into()); args.push("-d".into()); args.push("raid1".into()); } // Note: compression is a mount-time option for btrfs; we will apply it in mount phase. // Leaving mkfs-time compression unset by design. // Force formatting in apply path to avoid leftover signatures on freshly created partitions. // Safe because we just created these partitions in this run. args.push("-f".into()); args.extend(spec.devices.iter().cloned()); let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); run_cmd(&args_ref)?; // Capture UUID from the first device let dev0 = &spec.devices[0]; let uuid = capture_uuid(&blkid, dev0)?; results.push(FsResult { kind: FsKind::Btrfs, devices: spec.devices.clone(), uuid, label: spec.label.clone(), }); } FsKind::Bcachefs => { let Some(ref mkfs) = bcachefs_tool else { return Err(Error::Filesystem("bcachefs not found in PATH".into())); }; if spec.devices.len() < 2 { return Err(Error::Filesystem("bcachefs requires at least two devices (cache + backing)".into())); } // bcachefs format --label LABEL dev_cache dev_backing ... // TODO(fs): map compression/checksum/cache-mode flags from config in a follow-up. // This is deferred per current scope to focus on btrfs RAID profile wiring. let mut args: Vec = vec![mkfs.clone(), "format".into(), "--label".into(), spec.label.clone()]; args.extend(spec.devices.iter().cloned()); let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); run_cmd(&args_ref)?; // Capture UUID from the first device let dev0 = &spec.devices[0]; let uuid = capture_uuid(&blkid, dev0)?; results.push(FsResult { kind: FsKind::Bcachefs, devices: spec.devices.clone(), uuid, label: spec.label.clone(), }); } } } debug!("make_filesystems: created {} filesystems", results.len()); Ok(results) } fn capture_uuid(blkid: &str, dev: &str) -> Result { // blkid -o export /dev/... let out = run_cmd_capture(&[blkid, "-o", "export", dev])?; let map = parse_blkid_export(&out.stdout); // Prefer ID_FS_UUID if present, fall back to UUID if let Some(u) = map.get("ID_FS_UUID") { return Ok(u.clone()); } if let Some(u) = map.get("UUID") { return Ok(u.clone()); } warn!("blkid did not report UUID for {}", dev); Err(Error::Filesystem(format!("missing UUID in blkid output for {}", dev))) } /// Minimal parser for blkid -o export KEY=VAL lines. fn parse_blkid_export(s: &str) -> std::collections::HashMap { let mut map = std::collections::HashMap::new(); for line in s.lines() { if let Some((k, v)) = line.split_once('=') { map.insert(k.trim().to_string(), v.trim().to_string()); } } map } #[cfg(test)] mod tests_parse { use super::parse_blkid_export; #[test] fn parse_export_ok() { let s = "ID_FS_UUID=abcd-1234\nUUID=abcd-1234\nTYPE=btrfs\n"; let m = parse_blkid_export(s); assert_eq!(m.get("ID_FS_UUID").unwrap(), "abcd-1234"); assert_eq!(m.get("TYPE").unwrap(), "btrfs"); } }