// 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, cfg: &crate::types::Config) -> 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::{ Error, Result, partition::{PartRole, PartitionResult}, types::{Config, Topology}, util::{run_cmd, run_cmd_capture, which_tool}, }; use std::fs; 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(), }); } Topology::Bcachefs2Copy => { // Group all Data partitions into a single Bcachefs filesystem across multiple devices (2-copy semantics). 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( "Bcachefs2Copy topology requires at least 2 data partitions".to_string(), )); } specs.push(FsSpec { kind: FsKind::Bcachefs, devices: data_devs, label: cfg.filesystem.bcachefs.label.clone(), }); } Topology::BcachefsSingle => { // Single-device bcachefs on the sole Data partition. let data = parts .iter() .find(|p| matches!(p.role, PartRole::Data)) .ok_or_else(|| { Error::Filesystem( "expected a Data partition for BcachefsSingle topology".to_string(), ) })?; specs.push(FsSpec { kind: FsKind::Bcachefs, devices: vec![data.device_path.clone()], label: cfg.filesystem.bcachefs.label.clone(), }); } Topology::BtrfsSingle | Topology::DualIndependent => { // Map Data partition(s) to Btrfs (single device per partition for DualIndependent). 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, cfg: &Config) -> 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.is_empty() { return Err(Error::Filesystem( "bcachefs requires at least one device".into(), )); } // bcachefs format --label LABEL [--replicas=2] dev1 [dev2 ...] // Apply replicas policy for Bcachefs2Copy topology (data+metadata replicas = 2) let mut args: Vec = vec![ mkfs.clone(), "format".into(), "--label".into(), spec.label.clone(), ]; if matches!(cfg.topology, Topology::Bcachefs2Copy) { args.push("--replicas=2".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::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 } /// Probe existing filesystems on the system and return their identities (kind, uuid, label). /// /// This inspects /proc/partitions and uses `blkid -o export` on each device to detect: /// - Data filesystems: Btrfs or Bcachefs with label "ZOSDATA" /// - ESP filesystems: Vfat with label "ZOSBOOT" /// Multi-device filesystems (e.g., btrfs) are de-duplicated by UUID. /// /// Returns: /// - Vec with at most one entry per filesystem UUID. pub fn probe_existing_filesystems() -> Result> { let Some(blkid) = which_tool("blkid")? else { return Err(Error::Filesystem( "blkid not found in PATH; cannot probe existing filesystems".into(), )); }; let content = fs::read_to_string("/proc/partitions") .map_err(|e| Error::Filesystem(format!("/proc/partitions read error: {}", e)))?; let mut results_by_uuid: std::collections::HashMap = std::collections::HashMap::new(); for line in content.lines() { let line = line.trim(); if line.is_empty() || line.starts_with("major") { continue; } // Format: major minor #blocks name let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { continue; } let name = parts[3]; // Skip pseudo devices commonly not relevant (loop, ram, zram, fd) if name.starts_with("loop") || name.starts_with("ram") || name.starts_with("zram") || name.starts_with("fd") { continue; } let dev_path = format!("/dev/{}", name); // Probe with blkid -o export; ignore non-zero statuses meaning "nothing found" let out = match run_cmd_capture(&[blkid.as_str(), "-o", "export", dev_path.as_str()]) { Ok(o) => o, Err(Error::Tool { status, .. }) if status != 0 => { // No recognizable signature; skip continue; } Err(_) => { // Unexpected failure; skip this device continue; } }; let map = parse_blkid_export(&out.stdout); let ty = map.get("TYPE").cloned().unwrap_or_default(); let label = map .get("ID_FS_LABEL") .cloned() .or_else(|| map.get("LABEL").cloned()) .unwrap_or_default(); let uuid = map .get("ID_FS_UUID") .cloned() .or_else(|| map.get("UUID").cloned()); let (kind_opt, expected_label) = match ty.as_str() { "btrfs" => (Some(FsKind::Btrfs), "ZOSDATA"), "bcachefs" => (Some(FsKind::Bcachefs), "ZOSDATA"), "vfat" => (Some(FsKind::Vfat), "ZOSBOOT"), _ => (None, ""), }; if let (Some(kind), Some(u)) = (kind_opt, uuid) { // Enforce reserved label semantics if !expected_label.is_empty() && label != expected_label { continue; } // Deduplicate multi-device filesystems by UUID; record first-seen device results_by_uuid.entry(u.clone()).or_insert(FsResult { kind, devices: vec![dev_path.clone()], uuid: u, label: label.clone(), }); } } Ok(results_by_uuid.into_values().collect()) } #[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"); } }