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)]
|
||||
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)
|
||||
#[arg(long = "show", default_value_t = false)]
|
||||
pub show: bool,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! Configuration loading, merging, and validation (loader).
|
||||
//!
|
||||
//! Precedence (highest to lowest):
|
||||
//! - Kernel cmdline key `zosstorage.config=`
|
||||
//! - CLI flags
|
||||
//! - On-disk config file at /etc/zosstorage/config.yaml (if present)
|
||||
//! - CLI flags (and optional `--config PATH` when provided)
|
||||
//! - Kernel cmdline key `zosstorage.topo=`
|
||||
//! - Built-in defaults
|
||||
//!
|
||||
//! See [docs/SCHEMA.md](../../docs/SCHEMA.md) for the schema details.
|
||||
@@ -26,7 +25,7 @@
|
||||
// REGION: EXTENSION_POINTS-END
|
||||
//
|
||||
// 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.
|
||||
// REGION: SAFETY-END
|
||||
//
|
||||
@@ -45,17 +44,18 @@ use std::path::Path;
|
||||
|
||||
use crate::{cli::Cli, Error, Result};
|
||||
use crate::types::*;
|
||||
use serde_json::{Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
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.
|
||||
///
|
||||
/// Behavior:
|
||||
/// - Starts from built-in defaults (documented in docs/SCHEMA.md)
|
||||
/// - If /etc/zosstorage/config.yaml exists, merge it
|
||||
/// - If CLI --config is provided, merge that (overrides file defaults)
|
||||
/// - If kernel cmdline provides `zosstorage.config=...`, merge that last (highest precedence)
|
||||
/// - Skips implicit /etc reads in initramfs
|
||||
/// - If CLI --config is provided, merge that (overrides defaults)
|
||||
/// - If kernel cmdline provides `zosstorage.topo=...` and CLI did NOT specify `--topology`, apply it
|
||||
/// - Returns Error::Unimplemented when --force is used
|
||||
pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
||||
if cli.force {
|
||||
@@ -65,12 +65,8 @@ pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
||||
// 1) Start with defaults
|
||||
let mut merged = to_value(default_config())?;
|
||||
|
||||
// 2) Merge default on-disk config if present
|
||||
let default_cfg_path = "/etc/zosstorage/config.yaml";
|
||||
if Path::new(default_cfg_path).exists() {
|
||||
let v = load_yaml_value(default_cfg_path)?;
|
||||
merge_value(&mut merged, v);
|
||||
}
|
||||
// 2) (initramfs) Skipped reading default on-disk config to avoid dependency on /etc.
|
||||
// If a config is needed, pass it via --config PATH or kernel cmdline `zosstorage.config=...`.
|
||||
|
||||
// 3) Merge CLI referenced config (if any)
|
||||
if let Some(cfg_path) = &cli.config {
|
||||
@@ -82,25 +78,17 @@ pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
||||
let cli_overlay = cli_overlay_value(cli);
|
||||
merge_value(&mut merged, cli_overlay);
|
||||
|
||||
// 5) Merge kernel cmdline referenced config (if any)
|
||||
if let Some(src) = kernel_cmdline_config_source()? {
|
||||
match src {
|
||||
KernelConfigSource::Path(kpath) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 5) Kernel cmdline topology (only if CLI did not specify topology), e.g., `zosstorage.topo=dual-independent`
|
||||
if cli.topology.is_none() {
|
||||
if let Some(topo) = kernel_cmdline_topology() {
|
||||
merge_value(&mut merged, json!({"topology": topo.to_string()}));
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize
|
||||
let cfg: Config = serde_json::from_value(merged).map_err(|e| Error::Other(e.into()))?;
|
||||
validate(&cfg)?;
|
||||
Ok(cfg)
|
||||
// Finalize
|
||||
let cfg: Config = serde_json::from_value(merged).map_err(|e| Error::Other(e.into()))?;
|
||||
validate(&cfg)?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
/// Validate semantic correctness of the configuration.
|
||||
@@ -331,6 +319,38 @@ fn kernel_cmdline_config_source() -> Result<Option<KernelConfigSource>> {
|
||||
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).
|
||||
fn default_config() -> Config {
|
||||
Config {
|
||||
@@ -354,7 +374,7 @@ fn default_config() -> Config {
|
||||
allow_removable: false,
|
||||
min_size_gib: 10,
|
||||
},
|
||||
topology: Topology::BtrfsSingle,
|
||||
topology: Topology::DualIndependent,
|
||||
partitioning: Partitioning {
|
||||
alignment_mib: 1,
|
||||
require_empty_disks: true,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -52,6 +52,8 @@ fn real_main() -> Result<()> {
|
||||
let ctx = orchestrator::Context::new(cfg, log_opts)
|
||||
.with_show(cli.show)
|
||||
.with_apply(cli.apply)
|
||||
.with_mount_existing(cli.mount_existing)
|
||||
.with_report_current(cli.report_current)
|
||||
.with_report_path(cli.report.clone());
|
||||
orchestrator::run(&ctx)
|
||||
}
|
||||
|
||||
@@ -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