mount: prefer boot disk ESP and run cargo fmt
* choose ESP matching the primary data disk when multiple ESPs exist, falling back gracefully for single-disk layouts * keep new helper to normalize device names and reuse the idempotent mount logic * apply cargo fmt across the tree
This commit is contained in:
169
src/fs/plan.rs
169
src/fs/plan.rs
@@ -18,21 +18,21 @@
|
||||
// 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: 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
|
||||
// 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)
|
||||
@@ -42,14 +42,13 @@
|
||||
//! [fn make_filesystems](plan.rs:1).
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
Error, Result,
|
||||
partition::{PartRole, PartitionResult},
|
||||
types::{Config, Topology},
|
||||
partition::{PartitionResult, PartRole},
|
||||
util::{run_cmd, run_cmd_capture, which_tool},
|
||||
Error,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
use std::fs;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// Filesystem kinds supported by zosstorage.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -97,17 +96,14 @@ pub struct FsResult {
|
||||
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<FsPlan> {
|
||||
/// 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<FsPlan> {
|
||||
let mut specs: Vec<FsSpec> = Vec::new();
|
||||
|
||||
// Always map ESP partitions
|
||||
@@ -122,10 +118,22 @@ pub fn plan_filesystems(
|
||||
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()))?;
|
||||
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,
|
||||
@@ -173,8 +181,14 @@ pub fn plan_filesystems(
|
||||
}
|
||||
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()))?;
|
||||
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()],
|
||||
@@ -194,7 +208,9 @@ pub fn plan_filesystems(
|
||||
}
|
||||
|
||||
if specs.is_empty() {
|
||||
return Err(Error::Filesystem("no filesystems to create from provided partitions".to_string()));
|
||||
return Err(Error::Filesystem(
|
||||
"no filesystems to create from provided partitions".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(FsPlan { specs })
|
||||
@@ -215,7 +231,9 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
||||
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()));
|
||||
return Err(Error::Filesystem(
|
||||
"blkid not found in PATH; cannot capture filesystem UUIDs".into(),
|
||||
));
|
||||
}
|
||||
let blkid = blkid_tool.unwrap();
|
||||
|
||||
@@ -248,7 +266,9 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
||||
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()));
|
||||
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<String> = vec![mkfs.clone(), "-L".into(), spec.label.clone()];
|
||||
@@ -288,11 +308,18 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
||||
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()));
|
||||
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<String> = vec![mkfs.clone(), "format".into(), "--label".into(), spec.label.clone()];
|
||||
let mut args: Vec<String> = vec![
|
||||
mkfs.clone(),
|
||||
"format".into(),
|
||||
"--label".into(),
|
||||
spec.label.clone(),
|
||||
];
|
||||
if matches!(cfg.topology, Topology::Bcachefs2Copy) {
|
||||
args.push("--replicas=2".into());
|
||||
}
|
||||
@@ -318,29 +345,32 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
||||
}
|
||||
|
||||
fn capture_uuid(blkid: &str, dev: &str) -> Result<String> {
|
||||
// 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)))
|
||||
// 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<String, String> {
|
||||
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
|
||||
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).
|
||||
@@ -354,13 +384,16 @@ fn parse_blkid_export(s: &str) -> std::collections::HashMap<String, String> {
|
||||
/// - Vec<FsResult> with at most one entry per filesystem UUID.
|
||||
pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
||||
let Some(blkid) = which_tool("blkid")? else {
|
||||
return Err(Error::Filesystem("blkid not found in PATH; cannot probe existing filesystems".into()));
|
||||
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<String, FsResult> = std::collections::HashMap::new();
|
||||
let mut results_by_uuid: std::collections::HashMap<String, FsResult> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
@@ -399,11 +432,13 @@ pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
||||
let map = parse_blkid_export(&out.stdout);
|
||||
let ty = map.get("TYPE").cloned().unwrap_or_default();
|
||||
let label = map
|
||||
.get("ID_FS_LABEL").cloned()
|
||||
.get("ID_FS_LABEL")
|
||||
.cloned()
|
||||
.or_else(|| map.get("LABEL").cloned())
|
||||
.unwrap_or_default();
|
||||
let uuid = map
|
||||
.get("ID_FS_UUID").cloned()
|
||||
.get("ID_FS_UUID")
|
||||
.cloned()
|
||||
.or_else(|| map.get("UUID").cloned());
|
||||
|
||||
let (kind_opt, expected_label) = match ty.as_str() {
|
||||
@@ -434,13 +469,13 @@ pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_parse {
|
||||
use super::parse_blkid_export;
|
||||
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");
|
||||
}
|
||||
}
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user