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

@@ -69,6 +69,10 @@ pub struct Context {
pub show: bool,
/// When true, perform destructive actions (apply mode).
pub apply: bool,
/// When true, attempt to mount existing filesystems based on on-disk headers (non-destructive).
pub mount_existing: bool,
/// When true, emit a report of currently initialized filesystems and mounts (non-destructive).
pub report_current: bool,
/// Optional report path override (when provided by CLI --report).
pub report_path_override: Option<String>,
}
@@ -81,6 +85,8 @@ impl Context {
log,
show: false,
apply: false,
mount_existing: false,
report_current: false,
report_path_override: None,
}
}
@@ -118,6 +124,18 @@ impl Context {
self.report_path_override = path;
self
}
/// Enable or disable mount-existing mode (non-destructive).
pub fn with_mount_existing(mut self, mount_existing: bool) -> Self {
self.mount_existing = mount_existing;
self
}
/// Enable or disable reporting of current state (non-destructive).
pub fn with_report_current(mut self, report_current: bool) -> Self {
self.report_current = report_current;
self
}
}
/// Run the one-shot provisioning flow.
@@ -127,15 +145,164 @@ impl Context {
pub fn run(ctx: &Context) -> Result<()> {
info!("orchestrator: starting run() with topology {:?}", ctx.cfg.topology);
// Enforce mutually exclusive execution modes among: --mount-existing, --report-current, --apply
let selected_modes =
(ctx.mount_existing as u8) +
(ctx.report_current as u8) +
(ctx.apply as u8);
if selected_modes > 1 {
return Err(Error::Validation(
"choose only one mode: --mount-existing | --report-current | --apply".into(),
));
}
// Mode 1: Mount existing filesystems (non-destructive), based on on-disk headers.
if ctx.mount_existing {
info!("orchestrator: mount-existing mode");
let fs_results = zfs::probe_existing_filesystems()?;
if fs_results.is_empty() {
return Err(Error::Mount(
"no existing filesystems with reserved labels (ZOSBOOT/ZOSDATA) were found".into(),
));
}
let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?;
let mres = crate::mount::apply_mounts(&mplan)?;
crate::mount::maybe_write_fstab(&mres, &ctx.cfg)?;
// Optional JSON summary for mount-existing
if ctx.show || ctx.report_path_override.is_some() || ctx.report_current {
let now = format_rfc3339(SystemTime::now()).to_string();
let fs_json: Vec<serde_json::Value> = fs_results
.iter()
.map(|r| {
let kind_str = match r.kind {
zfs::FsKind::Vfat => "vfat",
zfs::FsKind::Btrfs => "btrfs",
zfs::FsKind::Bcachefs => "bcachefs",
};
json!({
"kind": kind_str,
"uuid": r.uuid,
"label": r.label,
"devices": r.devices,
})
})
.collect();
let mounts_json: Vec<serde_json::Value> = mres
.iter()
.map(|m| {
json!({
"source": m.source,
"target": m.target,
"fstype": m.fstype,
"options": m.options,
})
})
.collect();
let summary = json!({
"version": "v1",
"timestamp": now,
"status": "mounted_existing",
"filesystems": fs_json,
"mounts": mounts_json,
});
if ctx.show || ctx.report_current {
println!("{}", summary);
}
if let Some(path) = &ctx.report_path_override {
fs::write(path, summary.to_string()).map_err(|e| {
Error::Report(format!("failed to write report to {}: {}", path, e))
})?;
info!("orchestrator: wrote mount-existing report to {}", path);
}
}
return Ok(());
}
// Mode 3: Report current initialized filesystems and mounts (non-destructive).
if ctx.report_current {
info!("orchestrator: report-current mode");
let fs_results = zfs::probe_existing_filesystems()?;
// Parse /proc/mounts and include only our relevant targets.
let mounts_content = fs::read_to_string("/proc/mounts").unwrap_or_default();
let mounts_json: Vec<serde_json::Value> = mounts_content
.lines()
.filter_map(|line| {
let mut it = line.split_whitespace();
let source = it.next()?;
let target = it.next()?;
let fstype = it.next()?;
let options = it.next().unwrap_or("");
if target.starts_with("/var/mounts/")
|| target == "/var/cache/system"
|| target == "/var/cache/etc"
|| target == "/var/cache/modules"
|| target == "/var/cache/vm-meta"
{
Some(json!({
"source": source,
"target": target,
"fstype": fstype,
"options": options
}))
} else {
None
}
})
.collect();
let fs_json: Vec<serde_json::Value> = fs_results
.iter()
.map(|r| {
let kind_str = match r.kind {
zfs::FsKind::Vfat => "vfat",
zfs::FsKind::Btrfs => "btrfs",
zfs::FsKind::Bcachefs => "bcachefs",
};
json!({
"kind": kind_str,
"uuid": r.uuid,
"label": r.label,
"devices": r.devices
})
})
.collect();
let now = format_rfc3339(SystemTime::now()).to_string();
let summary = json!({
"version": "v1",
"timestamp": now,
"status": "observed",
"filesystems": fs_json,
"mounts": mounts_json
});
// In report-current mode, default to stdout; also honor --report path when provided.
println!("{}", summary);
if let Some(path) = &ctx.report_path_override {
fs::write(path, summary.to_string()).map_err(|e| {
Error::Report(format!("failed to write report to {}: {}", path, e))
})?;
info!("orchestrator: wrote report-current to {}", path);
}
return Ok(());
}
// Default path: plan (and optionally apply) for empty-disk initialization workflow.
// 1) Idempotency pre-flight: if already provisioned, optionally emit summary then exit success.
match idempotency::detect_existing_state()? {
Some(state) => {
info!("orchestrator: already provisioned");
if ctx.show || ctx.report_path_override.is_some() {
let now = format_rfc3339(SystemTime::now()).to_string();
let state_json = to_value(&state).map_err(|e| {
Error::Report(format!("failed to serialize StateReport: {}", e))
})?;
let state_json = to_value(&state)
.map_err(|e| Error::Report(format!("failed to serialize StateReport: {}", e)))?;
let summary = json!({
"version": "v1",
"timestamp": now,
@@ -146,8 +313,9 @@ pub fn run(ctx: &Context) -> Result<()> {
println!("{}", summary);
}
if let Some(path) = &ctx.report_path_override {
fs::write(path, summary.to_string())
.map_err(|e| Error::Report(format!("failed to write report to {}: {}", path, e)))?;
fs::write(path, summary.to_string()).map_err(|e| {
Error::Report(format!("failed to write report to {}: {}", path, e))
})?;
info!("orchestrator: wrote idempotency report to {}", path);
}
}
@@ -174,7 +342,7 @@ pub fn run(ctx: &Context) -> Result<()> {
warn!("orchestrator: require_empty_disks=false; proceeding without emptiness enforcement");
}
// 4) Partition planning (declarative only; application not yet implemented in this step).
// 4) Partition planning (declarative).
let plan = partition::plan_partitions(&disks, &ctx.cfg)?;
debug!(
"orchestrator: partition plan ready (alignment={} MiB, disks={})",
@@ -197,7 +365,10 @@ pub fn run(ctx: &Context) -> Result<()> {
// Filesystem planning and creation
let fs_plan = zfs::plan_filesystems(&part_results, &ctx.cfg)?;
info!("orchestrator: filesystem plan contains {} spec(s)", fs_plan.specs.len());
info!(
"orchestrator: filesystem plan contains {} spec(s)",
fs_plan.specs.len()
);
let fs_results = zfs::make_filesystems(&fs_plan, &ctx.cfg)?;
info!("orchestrator: created {} filesystem(s)", fs_results.len());