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:
@@ -87,6 +87,15 @@ pub struct Cli {
|
|||||||
#[arg(long = "allow-removable", default_value_t = false)]
|
#[arg(long = "allow-removable", default_value_t = false)]
|
||||||
pub allow_removable: bool,
|
pub allow_removable: bool,
|
||||||
|
|
||||||
|
/// Attempt to mount existing filesystems based on on-disk headers; no partitioning or mkfs.
|
||||||
|
/// Non-destructive mounting flow; uses UUID= sources and policy from config.
|
||||||
|
#[arg(long = "mount-existing", default_value_t = false)]
|
||||||
|
pub mount_existing: bool,
|
||||||
|
|
||||||
|
/// Report current initialized filesystems and mounts without performing changes.
|
||||||
|
#[arg(long = "report-current", default_value_t = false)]
|
||||||
|
pub report_current: bool,
|
||||||
|
|
||||||
/// Print detection and planning summary as JSON to stdout (non-default)
|
/// Print detection and planning summary as JSON to stdout (non-default)
|
||||||
#[arg(long = "show", default_value_t = false)]
|
#[arg(long = "show", default_value_t = false)]
|
||||||
pub show: bool,
|
pub show: bool,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
//! Configuration loading, merging, and validation (loader).
|
//! Configuration loading, merging, and validation (loader).
|
||||||
//!
|
//!
|
||||||
//! Precedence (highest to lowest):
|
//! Precedence (highest to lowest):
|
||||||
//! - Kernel cmdline key `zosstorage.config=`
|
//! - CLI flags (and optional `--config PATH` when provided)
|
||||||
//! - CLI flags
|
//! - Kernel cmdline key `zosstorage.topo=`
|
||||||
//! - On-disk config file at /etc/zosstorage/config.yaml (if present)
|
|
||||||
//! - Built-in defaults
|
//! - Built-in defaults
|
||||||
//!
|
//!
|
||||||
//! See [docs/SCHEMA.md](../../docs/SCHEMA.md) for the schema details.
|
//! See [docs/SCHEMA.md](../../docs/SCHEMA.md) for the schema details.
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
// REGION: EXTENSION_POINTS-END
|
// REGION: EXTENSION_POINTS-END
|
||||||
//
|
//
|
||||||
// REGION: SAFETY
|
// REGION: SAFETY
|
||||||
// safety: precedence enforced (kernel > CLI flags > CLI --config > /etc file > defaults).
|
// safety: precedence enforced (CLI flags > kernel cmdline > built-in defaults).
|
||||||
// safety: reserved GPT names and labels validated to avoid destructive operations later.
|
// safety: reserved GPT names and labels validated to avoid destructive operations later.
|
||||||
// REGION: SAFETY-END
|
// REGION: SAFETY-END
|
||||||
//
|
//
|
||||||
@@ -45,17 +44,18 @@ use std::path::Path;
|
|||||||
|
|
||||||
use crate::{cli::Cli, Error, Result};
|
use crate::{cli::Cli, Error, Result};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value, json};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
|
|
||||||
/// Load defaults, merge on-disk config, overlay CLI, and finally kernel cmdline key.
|
//// Load defaults, merge optional CLI --config, overlay CLI flags (highest precedence),
|
||||||
|
//// then consider kernel cmdline topology only if CLI omitted it.
|
||||||
/// Returns a validated Config on success.
|
/// Returns a validated Config on success.
|
||||||
///
|
///
|
||||||
/// Behavior:
|
/// Behavior:
|
||||||
/// - Starts from built-in defaults (documented in docs/SCHEMA.md)
|
/// - Starts from built-in defaults (documented in docs/SCHEMA.md)
|
||||||
/// - If /etc/zosstorage/config.yaml exists, merge it
|
/// - Skips implicit /etc reads in initramfs
|
||||||
/// - If CLI --config is provided, merge that (overrides file defaults)
|
/// - If CLI --config is provided, merge that (overrides defaults)
|
||||||
/// - If kernel cmdline provides `zosstorage.config=...`, merge that last (highest precedence)
|
/// - If kernel cmdline provides `zosstorage.topo=...` and CLI did NOT specify `--topology`, apply it
|
||||||
/// - Returns Error::Unimplemented when --force is used
|
/// - Returns Error::Unimplemented when --force is used
|
||||||
pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
||||||
if cli.force {
|
if cli.force {
|
||||||
@@ -65,12 +65,8 @@ pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
|||||||
// 1) Start with defaults
|
// 1) Start with defaults
|
||||||
let mut merged = to_value(default_config())?;
|
let mut merged = to_value(default_config())?;
|
||||||
|
|
||||||
// 2) Merge default on-disk config if present
|
// 2) (initramfs) Skipped reading default on-disk config to avoid dependency on /etc.
|
||||||
let default_cfg_path = "/etc/zosstorage/config.yaml";
|
// If a config is needed, pass it via --config PATH or kernel cmdline `zosstorage.config=...`.
|
||||||
if Path::new(default_cfg_path).exists() {
|
|
||||||
let v = load_yaml_value(default_cfg_path)?;
|
|
||||||
merge_value(&mut merged, v);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Merge CLI referenced config (if any)
|
// 3) Merge CLI referenced config (if any)
|
||||||
if let Some(cfg_path) = &cli.config {
|
if let Some(cfg_path) = &cli.config {
|
||||||
@@ -82,18 +78,10 @@ pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
|||||||
let cli_overlay = cli_overlay_value(cli);
|
let cli_overlay = cli_overlay_value(cli);
|
||||||
merge_value(&mut merged, cli_overlay);
|
merge_value(&mut merged, cli_overlay);
|
||||||
|
|
||||||
// 5) Merge kernel cmdline referenced config (if any)
|
// 5) Kernel cmdline topology (only if CLI did not specify topology), e.g., `zosstorage.topo=dual-independent`
|
||||||
if let Some(src) = kernel_cmdline_config_source()? {
|
if cli.topology.is_none() {
|
||||||
match src {
|
if let Some(topo) = kernel_cmdline_topology() {
|
||||||
KernelConfigSource::Path(kpath) => {
|
merge_value(&mut merged, json!({"topology": topo.to_string()}));
|
||||||
let v = load_yaml_value(&kpath)?;
|
|
||||||
merge_value(&mut merged, v);
|
|
||||||
}
|
|
||||||
KernelConfigSource::Data(yaml) => {
|
|
||||||
let v: serde_json::Value = serde_yaml::from_str(&yaml)
|
|
||||||
.map_err(|e| Error::Config(format!("failed to parse YAML from data: URL: {}", e)))?;
|
|
||||||
merge_value(&mut merged, v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +319,38 @@ fn kernel_cmdline_config_source() -> Result<Option<KernelConfigSource>> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kernel_cmdline_topology() -> Option<Topology> {
|
||||||
|
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
||||||
|
for token in cmdline.split_whitespace() {
|
||||||
|
if let Some(mut val) = token.strip_prefix("zosstorage.topo=") {
|
||||||
|
// Trim surrounding quotes if any
|
||||||
|
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
||||||
|
val = &val[1..val.len() - 1];
|
||||||
|
}
|
||||||
|
let val_norm = val.trim();
|
||||||
|
if let Some(t) = parse_topology_token(val_norm) {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to parse known topology tokens in kebab- or snake-case.
|
||||||
|
fn parse_topology_token(s: &str) -> Option<Topology> {
|
||||||
|
// Normalize underscores to hyphens for simpler matching.
|
||||||
|
let k = s.trim().to_ascii_lowercase().replace('_', "-");
|
||||||
|
match k.as_str() {
|
||||||
|
"btrfs-single" => Some(Topology::BtrfsSingle),
|
||||||
|
"bcachefs-single" => Some(Topology::BcachefsSingle),
|
||||||
|
"dual-independent" => Some(Topology::DualIndependent),
|
||||||
|
"ssd-hdd-bcachefs" => Some(Topology::SsdHddBcachefs),
|
||||||
|
"bcachefs2-copy" | "bcachefs-2copy" | "bcachefs-2-copy" => Some(Topology::Bcachefs2Copy),
|
||||||
|
"btrfs-raid1" => Some(Topology::BtrfsRaid1),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Built-in defaults for the entire configuration (schema version 1).
|
/// Built-in defaults for the entire configuration (schema version 1).
|
||||||
fn default_config() -> Config {
|
fn default_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
@@ -354,7 +374,7 @@ fn default_config() -> Config {
|
|||||||
allow_removable: false,
|
allow_removable: false,
|
||||||
min_size_gib: 10,
|
min_size_gib: 10,
|
||||||
},
|
},
|
||||||
topology: Topology::BtrfsSingle,
|
topology: Topology::DualIndependent,
|
||||||
partitioning: Partitioning {
|
partitioning: Partitioning {
|
||||||
alignment_mib: 1,
|
alignment_mib: 1,
|
||||||
require_empty_disks: true,
|
require_empty_disks: true,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ use crate::{
|
|||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
/// Filesystem kinds supported by zosstorage.
|
/// Filesystem kinds supported by zosstorage.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -342,6 +343,95 @@ fn parse_blkid_export(s: &str) -> std::collections::HashMap<String, String> {
|
|||||||
map
|
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)]
|
#[cfg(test)]
|
||||||
mod tests_parse {
|
mod tests_parse {
|
||||||
use super::parse_blkid_export;
|
use super::parse_blkid_export;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ fn real_main() -> Result<()> {
|
|||||||
let ctx = orchestrator::Context::new(cfg, log_opts)
|
let ctx = orchestrator::Context::new(cfg, log_opts)
|
||||||
.with_show(cli.show)
|
.with_show(cli.show)
|
||||||
.with_apply(cli.apply)
|
.with_apply(cli.apply)
|
||||||
|
.with_mount_existing(cli.mount_existing)
|
||||||
|
.with_report_current(cli.report_current)
|
||||||
.with_report_path(cli.report.clone());
|
.with_report_path(cli.report.clone());
|
||||||
orchestrator::run(&ctx)
|
orchestrator::run(&ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ pub struct Context {
|
|||||||
pub show: bool,
|
pub show: bool,
|
||||||
/// When true, perform destructive actions (apply mode).
|
/// When true, perform destructive actions (apply mode).
|
||||||
pub apply: bool,
|
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).
|
/// Optional report path override (when provided by CLI --report).
|
||||||
pub report_path_override: Option<String>,
|
pub report_path_override: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -81,6 +85,8 @@ impl Context {
|
|||||||
log,
|
log,
|
||||||
show: false,
|
show: false,
|
||||||
apply: false,
|
apply: false,
|
||||||
|
mount_existing: false,
|
||||||
|
report_current: false,
|
||||||
report_path_override: None,
|
report_path_override: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +124,18 @@ impl Context {
|
|||||||
self.report_path_override = path;
|
self.report_path_override = path;
|
||||||
self
|
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.
|
/// Run the one-shot provisioning flow.
|
||||||
@@ -127,15 +145,164 @@ impl Context {
|
|||||||
pub fn run(ctx: &Context) -> Result<()> {
|
pub fn run(ctx: &Context) -> Result<()> {
|
||||||
info!("orchestrator: starting run() with topology {:?}", ctx.cfg.topology);
|
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.
|
// 1) Idempotency pre-flight: if already provisioned, optionally emit summary then exit success.
|
||||||
match idempotency::detect_existing_state()? {
|
match idempotency::detect_existing_state()? {
|
||||||
Some(state) => {
|
Some(state) => {
|
||||||
info!("orchestrator: already provisioned");
|
info!("orchestrator: already provisioned");
|
||||||
if ctx.show || ctx.report_path_override.is_some() {
|
if ctx.show || ctx.report_path_override.is_some() {
|
||||||
let now = format_rfc3339(SystemTime::now()).to_string();
|
let now = format_rfc3339(SystemTime::now()).to_string();
|
||||||
let state_json = to_value(&state).map_err(|e| {
|
let state_json = to_value(&state)
|
||||||
Error::Report(format!("failed to serialize StateReport: {}", e))
|
.map_err(|e| Error::Report(format!("failed to serialize StateReport: {}", e)))?;
|
||||||
})?;
|
|
||||||
let summary = json!({
|
let summary = json!({
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
"timestamp": now,
|
"timestamp": now,
|
||||||
@@ -146,8 +313,9 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
println!("{}", summary);
|
println!("{}", summary);
|
||||||
}
|
}
|
||||||
if let Some(path) = &ctx.report_path_override {
|
if let Some(path) = &ctx.report_path_override {
|
||||||
fs::write(path, summary.to_string())
|
fs::write(path, summary.to_string()).map_err(|e| {
|
||||||
.map_err(|e| Error::Report(format!("failed to write report to {}: {}", path, e)))?;
|
Error::Report(format!("failed to write report to {}: {}", path, e))
|
||||||
|
})?;
|
||||||
info!("orchestrator: wrote idempotency report to {}", path);
|
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");
|
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)?;
|
let plan = partition::plan_partitions(&disks, &ctx.cfg)?;
|
||||||
debug!(
|
debug!(
|
||||||
"orchestrator: partition plan ready (alignment={} MiB, disks={})",
|
"orchestrator: partition plan ready (alignment={} MiB, disks={})",
|
||||||
@@ -197,7 +365,10 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
|
|
||||||
// Filesystem planning and creation
|
// Filesystem planning and creation
|
||||||
let fs_plan = zfs::plan_filesystems(&part_results, &ctx.cfg)?;
|
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)?;
|
let fs_results = zfs::make_filesystems(&fs_plan, &ctx.cfg)?;
|
||||||
info!("orchestrator: created {} filesystem(s)", fs_results.len());
|
info!("orchestrator: created {} filesystem(s)", fs_results.len());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user