feat(orchestrator,cli,config,fs): implement 3 modes, CLI-first precedence, kernel topo, defaults

- Orchestrator:
  - Add mutually exclusive modes: --mount-existing, --report-current, --apply
  - Wire mount-existing/report-current flows and JSON summaries
  - Reuse mount planning/application; never mount ESP
  - Context builders for new flags
  (see: src/orchestrator/run.rs:1)

- CLI:
  - Add --mount-existing and --report-current flags
  - Keep -t/--topology (ValueEnum) as before
  (see: src/cli/args.rs:1)

- FS:
  - Implement probe_existing_filesystems() using blkid to detect ZOSDATA/ZOSBOOT and dedupe by UUID
  (see: src/fs/plan.rs:1)

- Config loader:
  - Precedence now: CLI flags > kernel cmdline (zosstorage.topo) > built-in defaults
  - Read kernel cmdline topology only if CLI didn’t set -t/--topology
  - Default topology set to DualIndependent
  - Do not read /etc config by default in initramfs
  (see: src/config/loader.rs:1)

- Main:
  - Wire new Context builder flags
  (see: src/main.rs:1)

Rationale:
- Enables running from in-kernel initramfs with no config file
- Topology can be selected via kernel cmdline (zosstorage.topo) or CLI; CLI has priority
This commit is contained in:
2025-09-30 16:16:51 +02:00
parent b0d8c0bc75
commit d374176c0b
5 changed files with 336 additions and 44 deletions

View File

@@ -49,6 +49,7 @@ use crate::{
Error,
};
use tracing::{debug, warn};
use std::fs;
/// Filesystem kinds supported by zosstorage.
#[derive(Debug, Clone, Copy)]
@@ -342,6 +343,95 @@ fn parse_blkid_export(s: &str) -> std::collections::HashMap<String, 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<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()));
};
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();
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;