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:
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user