mount: prefer boot disk ESP and run cargo fmt
* choose ESP matching the primary data disk when multiple ESPs exist, falling back gracefully for single-disk layouts * keep new helper to normalize device names and reuse the idempotent mount logic * apply cargo fmt across the tree
This commit is contained in:
@@ -42,8 +42,8 @@
|
|||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use crate::{cli::Cli, Error, Result};
|
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
use crate::{Error, Result, cli::Cli};
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -181,7 +181,9 @@ pub fn validate(cfg: &Config) -> Result<()> {
|
|||||||
Topology::Bcachefs2Copy => {}
|
Topology::Bcachefs2Copy => {}
|
||||||
Topology::BtrfsRaid1 => {
|
Topology::BtrfsRaid1 => {
|
||||||
// No enforced requirement here beyond presence of two disks at runtime.
|
// No enforced requirement here beyond presence of two disks at runtime.
|
||||||
if cfg.filesystem.btrfs.raid_profile != "raid1" && cfg.filesystem.btrfs.raid_profile != "none" {
|
if cfg.filesystem.btrfs.raid_profile != "raid1"
|
||||||
|
&& cfg.filesystem.btrfs.raid_profile != "none"
|
||||||
|
{
|
||||||
return Err(Error::Validation(
|
return Err(Error::Validation(
|
||||||
"filesystem.btrfs.raid_profile must be 'none' or 'raid1'".into(),
|
"filesystem.btrfs.raid_profile must be 'none' or 'raid1'".into(),
|
||||||
));
|
));
|
||||||
@@ -203,7 +205,6 @@ fn to_value<T: serde::Serialize>(t: T) -> Result<Value> {
|
|||||||
serde_json::to_value(t).map_err(|e| Error::Other(e.into()))
|
serde_json::to_value(t).map_err(|e| Error::Other(e.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Merge b into a in-place:
|
/// Merge b into a in-place:
|
||||||
/// - Objects are merged key-by-key (recursively)
|
/// - Objects are merged key-by-key (recursively)
|
||||||
/// - Arrays and scalars replace
|
/// - Arrays and scalars replace
|
||||||
@@ -270,7 +271,9 @@ pub fn kernel_cmdline_topology() -> Option<Topology> {
|
|||||||
val_opt = Some(v);
|
val_opt = Some(v);
|
||||||
}
|
}
|
||||||
if let Some(mut val) = val_opt {
|
if let Some(mut val) = val_opt {
|
||||||
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
if (val.starts_with('"') && val.ends_with('"'))
|
||||||
|
|| (val.starts_with('\'') && val.ends_with('\''))
|
||||||
|
{
|
||||||
val = &val[1..val.len() - 1];
|
val = &val[1..val.len() - 1];
|
||||||
}
|
}
|
||||||
let val_norm = val.trim();
|
let val_norm = val.trim();
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
|
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
|
|
||||||
pub use loader::{load_and_merge, validate};
|
|
||||||
pub use crate::types::*;
|
pub use crate::types::*;
|
||||||
|
pub use loader::{load_and_merge, validate};
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ pub fn discover(filter: &DeviceFilter) -> Result<Vec<Disk>> {
|
|||||||
discover_with_provider(&provider, filter)
|
discover_with_provider(&provider, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_with_provider<P: DeviceProvider>(provider: &P, filter: &DeviceFilter) -> Result<Vec<Disk>> {
|
fn discover_with_provider<P: DeviceProvider>(
|
||||||
|
provider: &P,
|
||||||
|
filter: &DeviceFilter,
|
||||||
|
) -> Result<Vec<Disk>> {
|
||||||
let mut candidates = provider.list_block_devices()?;
|
let mut candidates = provider.list_block_devices()?;
|
||||||
// Probe properties if provider needs to enrich
|
// Probe properties if provider needs to enrich
|
||||||
for d in &mut candidates {
|
for d in &mut candidates {
|
||||||
@@ -210,10 +213,15 @@ fn discover_with_provider<P: DeviceProvider>(provider: &P, filter: &DeviceFilter
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if filtered.is_empty() {
|
if filtered.is_empty() {
|
||||||
return Err(Error::Device("no eligible disks found after applying filters".to_string()));
|
return Err(Error::Device(
|
||||||
|
"no eligible disks found after applying filters".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("eligible disks: {:?}", filtered.iter().map(|d| &d.path).collect::<Vec<_>>());
|
debug!(
|
||||||
|
"eligible disks: {:?}",
|
||||||
|
filtered.iter().map(|d| &d.path).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
Ok(filtered)
|
Ok(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,9 +267,10 @@ fn read_disk_size_bytes(name: &str) -> Result<u64> {
|
|||||||
let p = sys_block_path(name).join("size");
|
let p = sys_block_path(name).join("size");
|
||||||
let sectors = fs::read_to_string(&p)
|
let sectors = fs::read_to_string(&p)
|
||||||
.map_err(|e| Error::Device(format!("read {} failed: {}", p.display(), e)))?;
|
.map_err(|e| Error::Device(format!("read {} failed: {}", p.display(), e)))?;
|
||||||
let sectors: u64 = sectors.trim().parse().map_err(|e| {
|
let sectors: u64 = sectors
|
||||||
Error::Device(format!("parse sectors for {} failed: {}", name, e))
|
.trim()
|
||||||
})?;
|
.parse()
|
||||||
|
.map_err(|e| Error::Device(format!("parse sectors for {} failed: {}", name, e)))?;
|
||||||
Ok(sectors.saturating_mul(512))
|
Ok(sectors.saturating_mul(512))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,11 +296,7 @@ fn read_optional_string(p: PathBuf) -> Option<String> {
|
|||||||
while s.ends_with('\n') || s.ends_with('\r') {
|
while s.ends_with('\n') || s.ends_with('\r') {
|
||||||
s.pop();
|
s.pop();
|
||||||
}
|
}
|
||||||
if s.is_empty() {
|
if s.is_empty() { None } else { Some(s) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
@@ -324,9 +329,27 @@ mod tests {
|
|||||||
fn filter_by_size_and_include_exclude() {
|
fn filter_by_size_and_include_exclude() {
|
||||||
let provider = MockProvider {
|
let provider = MockProvider {
|
||||||
disks: vec![
|
disks: vec![
|
||||||
Disk { path: "/dev/sda".into(), size_bytes: 500 * 1024 * 1024 * 1024, rotational: true, model: None, serial: None }, // 500 GiB
|
Disk {
|
||||||
Disk { path: "/dev/nvme0n1".into(), size_bytes: 128 * 1024 * 1024 * 1024, rotational: false, model: None, serial: None }, // 128 GiB
|
path: "/dev/sda".into(),
|
||||||
Disk { path: "/dev/loop0".into(), size_bytes: 8 * 1024 * 1024 * 1024, rotational: false, model: None, serial: None }, // 8 GiB pseudo (but mock provider supplies it)
|
size_bytes: 500 * 1024 * 1024 * 1024,
|
||||||
|
rotational: true,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
}, // 500 GiB
|
||||||
|
Disk {
|
||||||
|
path: "/dev/nvme0n1".into(),
|
||||||
|
size_bytes: 128 * 1024 * 1024 * 1024,
|
||||||
|
rotational: false,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
}, // 128 GiB
|
||||||
|
Disk {
|
||||||
|
path: "/dev/loop0".into(),
|
||||||
|
size_bytes: 8 * 1024 * 1024 * 1024,
|
||||||
|
rotational: false,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
}, // 8 GiB pseudo (but mock provider supplies it)
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -346,7 +369,13 @@ mod tests {
|
|||||||
fn no_match_returns_error() {
|
fn no_match_returns_error() {
|
||||||
let provider = MockProvider {
|
let provider = MockProvider {
|
||||||
disks: vec![
|
disks: vec![
|
||||||
Disk { path: "/dev/sdb".into(), size_bytes: 50 * 1024 * 1024 * 1024, rotational: true, model: None, serial: None }, // 50 GiB
|
Disk {
|
||||||
|
path: "/dev/sdb".into(),
|
||||||
|
size_bytes: 50 * 1024 * 1024 * 1024,
|
||||||
|
rotational: true,
|
||||||
|
model: None,
|
||||||
|
serial: None,
|
||||||
|
}, // 50 GiB
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
115
src/fs/plan.rs
115
src/fs/plan.rs
@@ -18,21 +18,21 @@
|
|||||||
// ext: dry-run mode to emit mkfs commands without executing (future).
|
// ext: dry-run mode to emit mkfs commands without executing (future).
|
||||||
// REGION: EXTENSION_POINTS-END
|
// REGION: EXTENSION_POINTS-END
|
||||||
//
|
//
|
||||||
// REGION: SAFETY
|
// REGION: SAFETY
|
||||||
// safety: must not run mkfs on non-empty or unexpected partitions; assume prior validation enforced.
|
// safety: must not run mkfs on non-empty or unexpected partitions; assume prior validation enforced.
|
||||||
// safety: ensure labels follow reserved semantics (ZOSBOOT for ESP, ZOSDATA for all data FS).
|
// safety: ensure labels follow reserved semantics (ZOSBOOT for ESP, ZOSDATA for all data FS).
|
||||||
// safety: mkfs.btrfs uses -f in apply path immediately after partitioning to handle leftover signatures.
|
// safety: mkfs.btrfs uses -f in apply path immediately after partitioning to handle leftover signatures.
|
||||||
// REGION: SAFETY-END
|
// REGION: SAFETY-END
|
||||||
//
|
//
|
||||||
// REGION: ERROR_MAPPING
|
// REGION: ERROR_MAPPING
|
||||||
// errmap: external mkfs/blkid failures -> crate::Error::Tool with captured stderr.
|
// errmap: external mkfs/blkid failures -> crate::Error::Tool with captured stderr.
|
||||||
// errmap: planning mismatches -> crate::Error::Filesystem with context.
|
// errmap: planning mismatches -> crate::Error::Filesystem with context.
|
||||||
// REGION: ERROR_MAPPING-END
|
// REGION: ERROR_MAPPING-END
|
||||||
//
|
//
|
||||||
// REGION: TODO
|
// REGION: TODO
|
||||||
// todo: bcachefs tuning flags mapping from config (compression/checksum/cache_mode) deferred
|
// todo: bcachefs tuning flags mapping from config (compression/checksum/cache_mode) deferred
|
||||||
// todo: add UUID consistency checks across multi-device filesystems
|
// todo: add UUID consistency checks across multi-device filesystems
|
||||||
// REGION: TODO-END
|
// REGION: TODO-END
|
||||||
//! Filesystem planning and creation for zosstorage.
|
//! Filesystem planning and creation for zosstorage.
|
||||||
//!
|
//!
|
||||||
//! Maps partition results to concrete filesystems (vfat, btrfs, bcachefs)
|
//! Maps partition results to concrete filesystems (vfat, btrfs, bcachefs)
|
||||||
@@ -42,14 +42,13 @@
|
|||||||
//! [fn make_filesystems](plan.rs:1).
|
//! [fn make_filesystems](plan.rs:1).
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Result,
|
Error, Result,
|
||||||
|
partition::{PartRole, PartitionResult},
|
||||||
types::{Config, Topology},
|
types::{Config, Topology},
|
||||||
partition::{PartitionResult, PartRole},
|
|
||||||
util::{run_cmd, run_cmd_capture, which_tool},
|
util::{run_cmd, run_cmd_capture, which_tool},
|
||||||
Error,
|
|
||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Filesystem kinds supported by zosstorage.
|
/// Filesystem kinds supported by zosstorage.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -97,17 +96,14 @@ pub struct FsResult {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine which partitions get which filesystem based on topology.
|
/// Determine which partitions get which filesystem based on topology.
|
||||||
///
|
///
|
||||||
/// Rules:
|
/// Rules:
|
||||||
/// - ESP partitions => Vfat with label from cfg.filesystem.vfat.label (reserved "ZOSBOOT")
|
/// - ESP partitions => Vfat with label from cfg.filesystem.vfat.label (reserved "ZOSBOOT")
|
||||||
/// - Data partitions => Btrfs with label cfg.filesystem.btrfs.label ("ZOSDATA"), unless topology SsdHddBcachefs
|
/// - Data partitions => Btrfs with label cfg.filesystem.btrfs.label ("ZOSDATA"), unless topology SsdHddBcachefs
|
||||||
/// - SsdHddBcachefs => pair one Cache partition (SSD) with one Data partition (HDD) into one Bcachefs FsSpec with devices [cache, data] and label cfg.filesystem.bcachefs.label ("ZOSDATA")
|
/// - SsdHddBcachefs => pair one Cache partition (SSD) with one Data partition (HDD) into one Bcachefs FsSpec with devices [cache, data] and label cfg.filesystem.bcachefs.label ("ZOSDATA")
|
||||||
/// - DualIndependent/BtrfsRaid1 => map each Data partition to its own Btrfs FsSpec (raid profile concerns are handled later during mkfs)
|
/// - DualIndependent/BtrfsRaid1 => map each Data partition to its own Btrfs FsSpec (raid profile concerns are handled later during mkfs)
|
||||||
pub fn plan_filesystems(
|
pub fn plan_filesystems(parts: &[PartitionResult], cfg: &Config) -> Result<FsPlan> {
|
||||||
parts: &[PartitionResult],
|
|
||||||
cfg: &Config,
|
|
||||||
) -> Result<FsPlan> {
|
|
||||||
let mut specs: Vec<FsSpec> = Vec::new();
|
let mut specs: Vec<FsSpec> = Vec::new();
|
||||||
|
|
||||||
// Always map ESP partitions
|
// Always map ESP partitions
|
||||||
@@ -122,10 +118,22 @@ pub fn plan_filesystems(
|
|||||||
match cfg.topology {
|
match cfg.topology {
|
||||||
Topology::SsdHddBcachefs => {
|
Topology::SsdHddBcachefs => {
|
||||||
// Expect exactly one cache (SSD) and at least one data (HDD). Use the first data for pairing.
|
// Expect exactly one cache (SSD) and at least one data (HDD). Use the first data for pairing.
|
||||||
let cache = parts.iter().find(|p| matches!(p.role, PartRole::Cache))
|
let cache = parts
|
||||||
.ok_or_else(|| Error::Filesystem("expected a Cache partition for SsdHddBcachefs topology".to_string()))?;
|
.iter()
|
||||||
let data = parts.iter().find(|p| matches!(p.role, PartRole::Data))
|
.find(|p| matches!(p.role, PartRole::Cache))
|
||||||
.ok_or_else(|| Error::Filesystem("expected a Data partition for SsdHddBcachefs topology".to_string()))?;
|
.ok_or_else(|| {
|
||||||
|
Error::Filesystem(
|
||||||
|
"expected a Cache partition for SsdHddBcachefs topology".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let data = parts
|
||||||
|
.iter()
|
||||||
|
.find(|p| matches!(p.role, PartRole::Data))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Filesystem(
|
||||||
|
"expected a Data partition for SsdHddBcachefs topology".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
specs.push(FsSpec {
|
specs.push(FsSpec {
|
||||||
kind: FsKind::Bcachefs,
|
kind: FsKind::Bcachefs,
|
||||||
@@ -173,8 +181,14 @@ pub fn plan_filesystems(
|
|||||||
}
|
}
|
||||||
Topology::BcachefsSingle => {
|
Topology::BcachefsSingle => {
|
||||||
// Single-device bcachefs on the sole Data partition.
|
// Single-device bcachefs on the sole Data partition.
|
||||||
let data = parts.iter().find(|p| matches!(p.role, PartRole::Data))
|
let data = parts
|
||||||
.ok_or_else(|| Error::Filesystem("expected a Data partition for BcachefsSingle topology".to_string()))?;
|
.iter()
|
||||||
|
.find(|p| matches!(p.role, PartRole::Data))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Filesystem(
|
||||||
|
"expected a Data partition for BcachefsSingle topology".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
specs.push(FsSpec {
|
specs.push(FsSpec {
|
||||||
kind: FsKind::Bcachefs,
|
kind: FsKind::Bcachefs,
|
||||||
devices: vec![data.device_path.clone()],
|
devices: vec![data.device_path.clone()],
|
||||||
@@ -194,7 +208,9 @@ pub fn plan_filesystems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if specs.is_empty() {
|
if specs.is_empty() {
|
||||||
return Err(Error::Filesystem("no filesystems to create from provided partitions".to_string()));
|
return Err(Error::Filesystem(
|
||||||
|
"no filesystems to create from provided partitions".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(FsPlan { specs })
|
Ok(FsPlan { specs })
|
||||||
@@ -215,7 +231,9 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
|||||||
let blkid_tool = which_tool("blkid")?;
|
let blkid_tool = which_tool("blkid")?;
|
||||||
|
|
||||||
if blkid_tool.is_none() {
|
if blkid_tool.is_none() {
|
||||||
return Err(Error::Filesystem("blkid not found in PATH; cannot capture filesystem UUIDs".into()));
|
return Err(Error::Filesystem(
|
||||||
|
"blkid not found in PATH; cannot capture filesystem UUIDs".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let blkid = blkid_tool.unwrap();
|
let blkid = blkid_tool.unwrap();
|
||||||
|
|
||||||
@@ -248,7 +266,9 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
|||||||
return Err(Error::Filesystem("mkfs.btrfs not found in PATH".into()));
|
return Err(Error::Filesystem("mkfs.btrfs not found in PATH".into()));
|
||||||
};
|
};
|
||||||
if spec.devices.is_empty() {
|
if spec.devices.is_empty() {
|
||||||
return Err(Error::Filesystem("btrfs requires at least one device".into()));
|
return Err(Error::Filesystem(
|
||||||
|
"btrfs requires at least one device".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// mkfs.btrfs -L LABEL [ -m raid1 -d raid1 (when multi-device/raid1) ] dev1 [dev2 ...]
|
// mkfs.btrfs -L LABEL [ -m raid1 -d raid1 (when multi-device/raid1) ] dev1 [dev2 ...]
|
||||||
let mut args: Vec<String> = vec![mkfs.clone(), "-L".into(), spec.label.clone()];
|
let mut args: Vec<String> = vec![mkfs.clone(), "-L".into(), spec.label.clone()];
|
||||||
@@ -288,11 +308,18 @@ pub fn make_filesystems(plan: &FsPlan, cfg: &Config) -> Result<Vec<FsResult>> {
|
|||||||
return Err(Error::Filesystem("bcachefs not found in PATH".into()));
|
return Err(Error::Filesystem("bcachefs not found in PATH".into()));
|
||||||
};
|
};
|
||||||
if spec.devices.is_empty() {
|
if spec.devices.is_empty() {
|
||||||
return Err(Error::Filesystem("bcachefs requires at least one device".into()));
|
return Err(Error::Filesystem(
|
||||||
|
"bcachefs requires at least one device".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// bcachefs format --label LABEL [--replicas=2] dev1 [dev2 ...]
|
// bcachefs format --label LABEL [--replicas=2] dev1 [dev2 ...]
|
||||||
// Apply replicas policy for Bcachefs2Copy topology (data+metadata replicas = 2)
|
// Apply replicas policy for Bcachefs2Copy topology (data+metadata replicas = 2)
|
||||||
let mut args: Vec<String> = vec![mkfs.clone(), "format".into(), "--label".into(), spec.label.clone()];
|
let mut args: Vec<String> = vec![
|
||||||
|
mkfs.clone(),
|
||||||
|
"format".into(),
|
||||||
|
"--label".into(),
|
||||||
|
spec.label.clone(),
|
||||||
|
];
|
||||||
if matches!(cfg.topology, Topology::Bcachefs2Copy) {
|
if matches!(cfg.topology, Topology::Bcachefs2Copy) {
|
||||||
args.push("--replicas=2".into());
|
args.push("--replicas=2".into());
|
||||||
}
|
}
|
||||||
@@ -329,7 +356,10 @@ fn capture_uuid(blkid: &str, dev: &str) -> Result<String> {
|
|||||||
return Ok(u.clone());
|
return Ok(u.clone());
|
||||||
}
|
}
|
||||||
warn!("blkid did not report UUID for {}", dev);
|
warn!("blkid did not report UUID for {}", dev);
|
||||||
Err(Error::Filesystem(format!("missing UUID in blkid output for {}", dev)))
|
Err(Error::Filesystem(format!(
|
||||||
|
"missing UUID in blkid output for {}",
|
||||||
|
dev
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal parser for blkid -o export KEY=VAL lines.
|
/// Minimal parser for blkid -o export KEY=VAL lines.
|
||||||
@@ -354,13 +384,16 @@ fn parse_blkid_export(s: &str) -> std::collections::HashMap<String, String> {
|
|||||||
/// - Vec<FsResult> with at most one entry per filesystem UUID.
|
/// - Vec<FsResult> with at most one entry per filesystem UUID.
|
||||||
pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
||||||
let Some(blkid) = which_tool("blkid")? else {
|
let Some(blkid) = which_tool("blkid")? else {
|
||||||
return Err(Error::Filesystem("blkid not found in PATH; cannot probe existing filesystems".into()));
|
return Err(Error::Filesystem(
|
||||||
|
"blkid not found in PATH; cannot probe existing filesystems".into(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = fs::read_to_string("/proc/partitions")
|
let content = fs::read_to_string("/proc/partitions")
|
||||||
.map_err(|e| Error::Filesystem(format!("/proc/partitions read error: {}", e)))?;
|
.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();
|
let mut results_by_uuid: std::collections::HashMap<String, FsResult> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
@@ -399,11 +432,13 @@ pub fn probe_existing_filesystems() -> Result<Vec<FsResult>> {
|
|||||||
let map = parse_blkid_export(&out.stdout);
|
let map = parse_blkid_export(&out.stdout);
|
||||||
let ty = map.get("TYPE").cloned().unwrap_or_default();
|
let ty = map.get("TYPE").cloned().unwrap_or_default();
|
||||||
let label = map
|
let label = map
|
||||||
.get("ID_FS_LABEL").cloned()
|
.get("ID_FS_LABEL")
|
||||||
|
.cloned()
|
||||||
.or_else(|| map.get("LABEL").cloned())
|
.or_else(|| map.get("LABEL").cloned())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let uuid = map
|
let uuid = map
|
||||||
.get("ID_FS_UUID").cloned()
|
.get("ID_FS_UUID")
|
||||||
|
.cloned()
|
||||||
.or_else(|| map.get("UUID").cloned());
|
.or_else(|| map.get("UUID").cloned());
|
||||||
|
|
||||||
let (kind_opt, expected_label) = match ty.as_str() {
|
let (kind_opt, expected_label) = match ty.as_str() {
|
||||||
|
|||||||
@@ -28,14 +28,14 @@
|
|||||||
//! disks are empty before making any destructive changes.
|
//! disks are empty before making any destructive changes.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
device::Disk,
|
|
||||||
report::{StateReport, REPORT_VERSION},
|
|
||||||
util::{run_cmd_capture, which_tool},
|
|
||||||
Error, Result,
|
Error, Result,
|
||||||
|
device::Disk,
|
||||||
|
report::{REPORT_VERSION, StateReport},
|
||||||
|
util::{run_cmd_capture, which_tool},
|
||||||
};
|
};
|
||||||
|
use humantime::format_rfc3339;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
use std::{collections::HashMap, fs, path::Path};
|
||||||
use humantime::format_rfc3339;
|
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Return existing state if system is already provisioned; otherwise None.
|
/// Return existing state if system is already provisioned; otherwise None.
|
||||||
@@ -155,7 +155,10 @@ pub fn is_empty_disk(disk: &Disk) -> Result<bool> {
|
|||||||
|
|
||||||
// Probe with blkid -p
|
// Probe with blkid -p
|
||||||
let Some(blkid) = which_tool("blkid")? else {
|
let Some(blkid) = which_tool("blkid")? else {
|
||||||
warn!("blkid not found; conservatively treating {} as not empty", disk.path);
|
warn!(
|
||||||
|
"blkid not found; conservatively treating {} as not empty",
|
||||||
|
disk.path
|
||||||
|
);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,7 +240,11 @@ fn is_partition_of(base: &str, name: &str) -> bool {
|
|||||||
if name == base {
|
if name == base {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let ends_with_digit = base.chars().last().map(|c| c.is_ascii_digit()).unwrap_or(false);
|
let ends_with_digit = base
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.map(|c| c.is_ascii_digit())
|
||||||
|
.unwrap_or(false);
|
||||||
if ends_with_digit {
|
if ends_with_digit {
|
||||||
// nvme0n1 -> nvme0n1p1
|
// nvme0n1 -> nvme0n1p1
|
||||||
if name.starts_with(base) {
|
if name.starts_with(base) {
|
||||||
|
|||||||
18
src/lib.rs
18
src/lib.rs
@@ -1,18 +1,18 @@
|
|||||||
//! Crate root for zosstorage: one-shot disk provisioning utility for initramfs.
|
//! Crate root for zosstorage: one-shot disk provisioning utility for initramfs.
|
||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod logging;
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod partition;
|
|
||||||
pub mod fs;
|
|
||||||
pub mod mount;
|
|
||||||
pub mod report;
|
|
||||||
pub mod orchestrator;
|
|
||||||
pub mod idempotency;
|
|
||||||
pub mod util;
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod types; // top-level types (moved from config/types.rs for visibility)
|
pub mod fs;
|
||||||
|
pub mod idempotency;
|
||||||
|
pub mod logging;
|
||||||
|
pub mod mount;
|
||||||
|
pub mod orchestrator;
|
||||||
|
pub mod partition;
|
||||||
|
pub mod report;
|
||||||
|
pub mod types;
|
||||||
|
pub mod util; // top-level types (moved from config/types.rs for visibility)
|
||||||
|
|
||||||
pub use errors::{Error, Result};
|
pub use errors::{Error, Result};
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ use std::fs::OpenOptions;
|
|||||||
use std::io::{self};
|
use std::io::{self};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
use tracing_subscriber::fmt;
|
use tracing_subscriber::fmt;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::registry::Registry;
|
use tracing_subscriber::registry::Registry;
|
||||||
use tracing_subscriber::filter::LevelFilter;
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
/// Logging options resolved from CLI and/or config.
|
/// Logging options resolved from CLI and/or config.
|
||||||
@@ -116,19 +116,25 @@ pub fn init_logging(opts: &LogOptions) -> Result<()> {
|
|||||||
.with(stderr_layer)
|
.with(stderr_layer)
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
.try_init()
|
.try_init()
|
||||||
.map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e))
|
||||||
|
})?;
|
||||||
} else {
|
} else {
|
||||||
// Fall back to stderr-only if file cannot be opened
|
// Fall back to stderr-only if file cannot be opened
|
||||||
Registry::default()
|
Registry::default()
|
||||||
.with(stderr_layer)
|
.with(stderr_layer)
|
||||||
.try_init()
|
.try_init()
|
||||||
.map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Registry::default()
|
Registry::default()
|
||||||
.with(stderr_layer)
|
.with(stderr_layer)
|
||||||
.try_init()
|
.try_init()
|
||||||
.map_err(|e| crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::Other(anyhow::anyhow!("failed to set global logger: {}", e))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = INIT_GUARD.set(());
|
let _ = INIT_GUARD.set(());
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ fn real_main() -> Result<()> {
|
|||||||
.with_report_current(cli.report_current)
|
.with_report_current(cli.report_current)
|
||||||
.with_report_path(cli.report.clone())
|
.with_report_path(cli.report.clone())
|
||||||
.with_topology_from_cli(cli.topology.is_some())
|
.with_topology_from_cli(cli.topology.is_some())
|
||||||
.with_topology_from_cmdline(config::loader::kernel_cmdline_topology().is_some() && cli.topology.is_none());
|
.with_topology_from_cmdline(
|
||||||
|
config::loader::kernel_cmdline_topology().is_some() && cli.topology.is_none(),
|
||||||
|
);
|
||||||
orchestrator::run(&ctx)
|
orchestrator::run(&ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,13 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
Error, Result,
|
||||||
fs::{FsKind, FsResult},
|
fs::{FsKind, FsResult},
|
||||||
types::Config,
|
types::Config,
|
||||||
util::{run_cmd, run_cmd_capture, which_tool},
|
util::{run_cmd, run_cmd_capture, which_tool},
|
||||||
Error, Result,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{File, create_dir_all};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -120,6 +120,24 @@ fn source_matches_uuid(existing_source: &str, uuid: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn disk_of_device(dev: &str) -> Option<String> {
|
||||||
|
let path = Path::new(dev);
|
||||||
|
let name = path.file_name()?.to_str()?;
|
||||||
|
let mut cutoff = name.len();
|
||||||
|
while cutoff > 0 && name.as_bytes()[cutoff - 1].is_ascii_digit() {
|
||||||
|
cutoff -= 1;
|
||||||
|
}
|
||||||
|
if cutoff == name.len() {
|
||||||
|
return Some(dev.to_string());
|
||||||
|
}
|
||||||
|
let mut disk = name[..cutoff].to_string();
|
||||||
|
if disk.ends_with('p') {
|
||||||
|
disk.pop();
|
||||||
|
}
|
||||||
|
let parent = path.parent()?.to_str().unwrap_or("/dev");
|
||||||
|
Some(format!("{}/{}", parent, disk))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlannedMount {
|
pub struct PlannedMount {
|
||||||
pub uuid: String, // UUID string without prefix
|
pub uuid: String, // UUID string without prefix
|
||||||
@@ -202,11 +220,27 @@ pub fn plan_mounts(fs_results: &[FsResult], _cfg: &Config) -> Result<MountPlan>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine primary UUID
|
let primary = data[0];
|
||||||
let primary_uuid = Some(data[0].uuid.clone());
|
let primary_uuid = Some(primary.uuid.clone());
|
||||||
|
let primary_disk = primary.devices.first().and_then(|dev| disk_of_device(dev));
|
||||||
|
|
||||||
// Optional ESP (VFAT) mount at /boot
|
let mut chosen_esp: Option<&FsResult> = None;
|
||||||
if let Some(esp) = fs_results.iter().find(|r| matches!(r.kind, FsKind::Vfat)) {
|
let mut fallback_esp: Option<&FsResult> = None;
|
||||||
|
for esp in fs_results.iter().filter(|r| matches!(r.kind, FsKind::Vfat)) {
|
||||||
|
if fallback_esp.is_none() {
|
||||||
|
fallback_esp = Some(esp);
|
||||||
|
}
|
||||||
|
if let (Some(ref disk), Some(esp_disk)) = (
|
||||||
|
primary_disk.as_ref(),
|
||||||
|
esp.devices.first().and_then(|dev| disk_of_device(dev)),
|
||||||
|
) {
|
||||||
|
if esp_disk == **disk {
|
||||||
|
chosen_esp = Some(esp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(esp) = chosen_esp.or(fallback_esp) {
|
||||||
root_mounts.push(PlannedMount {
|
root_mounts.push(PlannedMount {
|
||||||
uuid: esp.uuid.clone(),
|
uuid: esp.uuid.clone(),
|
||||||
target: BOOT_TARGET.to_string(),
|
target: BOOT_TARGET.to_string(),
|
||||||
@@ -216,7 +250,6 @@ pub fn plan_mounts(fs_results: &[FsResult], _cfg: &Config) -> Result<MountPlan>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subvol mounts only from primary FS
|
// Subvol mounts only from primary FS
|
||||||
let primary = data[0];
|
|
||||||
let mut subvol_mounts: Vec<PlannedSubvolMount> = Vec::new();
|
let mut subvol_mounts: Vec<PlannedSubvolMount> = Vec::new();
|
||||||
let fstype = fstype_str(primary.kind).to_string();
|
let fstype = fstype_str(primary.kind).to_string();
|
||||||
// Option key differs per filesystem: btrfs uses subvol=, bcachefs uses X-mount.subdir=
|
// Option key differs per filesystem: btrfs uses subvol=, bcachefs uses X-mount.subdir=
|
||||||
@@ -360,14 +393,18 @@ pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
|||||||
if !exists {
|
if !exists {
|
||||||
// Create subvolume
|
// Create subvolume
|
||||||
let subvol_path = format!("{}/{}", root, sm.name);
|
let subvol_path = format!("{}/{}", root, sm.name);
|
||||||
let args = [btrfs_tool.as_str(), "subvolume", "create", subvol_path.as_str()];
|
let args = [
|
||||||
|
btrfs_tool.as_str(),
|
||||||
|
"subvolume",
|
||||||
|
"create",
|
||||||
|
subvol_path.as_str(),
|
||||||
|
];
|
||||||
run_cmd(&args)?;
|
run_cmd(&args)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if primary_kind == "bcachefs" {
|
} else if primary_kind == "bcachefs" {
|
||||||
let bcachefs_tool = which_tool("bcachefs")?.ok_or_else(|| {
|
let bcachefs_tool = which_tool("bcachefs")?
|
||||||
Error::Mount("required tool 'bcachefs' not found in PATH".into())
|
.ok_or_else(|| Error::Mount("required tool 'bcachefs' not found in PATH".into()))?;
|
||||||
})?;
|
|
||||||
for sm in &plan.subvol_mounts {
|
for sm in &plan.subvol_mounts {
|
||||||
if &sm.uuid != primary_uuid {
|
if &sm.uuid != primary_uuid {
|
||||||
continue;
|
continue;
|
||||||
@@ -479,10 +516,7 @@ pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> {
|
|||||||
let mut lines: Vec<String> = Vec::new();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
for m in root_entries.into_iter().chain(subvol_entries.into_iter()) {
|
for m in root_entries.into_iter().chain(subvol_entries.into_iter()) {
|
||||||
// m.source already "UUID=..."
|
// m.source already "UUID=..."
|
||||||
let line = format!(
|
let line = format!("{} {} {} {} 0 0", m.source, m.target, m.fstype, m.options);
|
||||||
"{} {} {} {} 0 0",
|
|
||||||
m.source, m.target, m.fstype, m.options
|
|
||||||
);
|
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,14 +43,13 @@
|
|||||||
//! - Report generation and write
|
//! - Report generation and write
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{Config, Topology},
|
Error, Result,
|
||||||
|
device::{DeviceFilter, Disk, discover},
|
||||||
|
fs as zfs, idempotency,
|
||||||
logging::LogOptions,
|
logging::LogOptions,
|
||||||
device::{discover, DeviceFilter, Disk},
|
|
||||||
idempotency,
|
|
||||||
partition,
|
partition,
|
||||||
report::StateReport,
|
report::StateReport,
|
||||||
fs as zfs,
|
types::{Config, Topology},
|
||||||
Error, Result,
|
|
||||||
};
|
};
|
||||||
use humantime::format_rfc3339;
|
use humantime::format_rfc3339;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -191,9 +190,7 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
info!("orchestrator: starting run()");
|
info!("orchestrator: starting run()");
|
||||||
|
|
||||||
let selected_modes =
|
let selected_modes =
|
||||||
(ctx.mount_existing as u8) +
|
(ctx.mount_existing as u8) + (ctx.report_current as u8) + (ctx.apply as u8);
|
||||||
(ctx.report_current as u8) +
|
|
||||||
(ctx.apply as u8);
|
|
||||||
if selected_modes > 1 {
|
if selected_modes > 1 {
|
||||||
return Err(Error::Validation(
|
return Err(Error::Validation(
|
||||||
"choose only one mode: --mount-existing | --report-current | --apply".into(),
|
"choose only one mode: --mount-existing | --report-current | --apply".into(),
|
||||||
@@ -242,7 +239,11 @@ fn auto_select_mode(ctx: &Context) -> Result<AutoSelection> {
|
|||||||
info!("orchestrator: provisioned state detected; attempting mount-existing flow");
|
info!("orchestrator: provisioned state detected; attempting mount-existing flow");
|
||||||
return Ok(AutoSelection {
|
return Ok(AutoSelection {
|
||||||
decision: AutoDecision::MountExisting,
|
decision: AutoDecision::MountExisting,
|
||||||
fs_results: if fs_results.is_empty() { None } else { Some(fs_results) },
|
fs_results: if fs_results.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(fs_results)
|
||||||
|
},
|
||||||
state: Some(state),
|
state: Some(state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -330,9 +331,8 @@ fn run_report_current(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()).map_err(|e| {
|
fs::write(path, summary.to_string())
|
||||||
Error::Report(format!("failed to write report to {}: {}", path, e))
|
.map_err(|e| Error::Report(format!("failed to write report to {}: {}", path, e)))?;
|
||||||
})?;
|
|
||||||
info!("orchestrator: wrote report-current to {}", path);
|
info!("orchestrator: wrote report-current to {}", path);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -409,9 +409,8 @@ fn run_mount_existing(
|
|||||||
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()).map_err(|e| {
|
fs::write(path, summary.to_string())
|
||||||
Error::Report(format!("failed to write report to {}: {}", path, e))
|
.map_err(|e| Error::Report(format!("failed to write report to {}: {}", path, e)))?;
|
||||||
})?;
|
|
||||||
info!("orchestrator: wrote mount-existing report to {}", path);
|
info!("orchestrator: wrote mount-existing report to {}", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +444,9 @@ fn run_provisioning(
|
|||||||
enforce_empty_disks(&disks)?;
|
enforce_empty_disks(&disks)?;
|
||||||
info!("orchestrator: all target disks verified empty");
|
info!("orchestrator: all target disks verified empty");
|
||||||
} else {
|
} else {
|
||||||
warn!("orchestrator: preview mode detected (--show/--report); skipping empty-disk enforcement");
|
warn!(
|
||||||
|
"orchestrator: preview mode detected (--show/--report); skipping empty-disk enforcement"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if matches!(mode, ProvisioningMode::Apply) {
|
} else if matches!(mode, ProvisioningMode::Apply) {
|
||||||
warn!("orchestrator: require_empty_disks=false; proceeding without emptiness enforcement");
|
warn!("orchestrator: require_empty_disks=false; proceeding without emptiness enforcement");
|
||||||
@@ -506,7 +507,9 @@ fn run_provisioning(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("orchestrator: pre-flight complete (idempotency checked, devices discovered, plan computed)");
|
info!(
|
||||||
|
"orchestrator: pre-flight complete (idempotency checked, devices discovered, plan computed)"
|
||||||
|
);
|
||||||
|
|
||||||
if preview_outputs {
|
if preview_outputs {
|
||||||
let summary = build_summary_json(&disks, &plan, &effective_cfg)?;
|
let summary = build_summary_json(&disks, &plan, &effective_cfg)?;
|
||||||
@@ -514,9 +517,8 @@ fn run_provisioning(
|
|||||||
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()).map_err(|e| {
|
fs::write(path, summary.to_string())
|
||||||
Error::Report(format!("failed to write report to {}: {}", path, e))
|
.map_err(|e| Error::Report(format!("failed to write report to {}: {}", path, e)))?;
|
||||||
})?;
|
|
||||||
info!("orchestrator: wrote summary report to {}", path);
|
info!("orchestrator: wrote summary report to {}", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,15 +538,13 @@ fn build_device_filter(cfg: &Config) -> Result<DeviceFilter> {
|
|||||||
let mut exclude = Vec::new();
|
let mut exclude = Vec::new();
|
||||||
|
|
||||||
for pat in &cfg.device_selection.include_patterns {
|
for pat in &cfg.device_selection.include_patterns {
|
||||||
let re = Regex::new(pat).map_err(|e| {
|
let re = Regex::new(pat)
|
||||||
Error::Validation(format!("invalid include regex '{}': {}", pat, e))
|
.map_err(|e| Error::Validation(format!("invalid include regex '{}': {}", pat, e)))?;
|
||||||
})?;
|
|
||||||
include.push(re);
|
include.push(re);
|
||||||
}
|
}
|
||||||
for pat in &cfg.device_selection.exclude_patterns {
|
for pat in &cfg.device_selection.exclude_patterns {
|
||||||
let re = Regex::new(pat).map_err(|e| {
|
let re = Regex::new(pat)
|
||||||
Error::Validation(format!("invalid exclude regex '{}': {}", pat, e))
|
.map_err(|e| Error::Validation(format!("invalid exclude regex '{}': {}", pat, e)))?;
|
||||||
})?;
|
|
||||||
exclude.push(re);
|
exclude.push(re);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +598,11 @@ fn role_str(role: partition::PartRole) -> &'static str {
|
|||||||
/// - mount: scheme summary and target template (e.g., "/var/cache/{UUID}")
|
/// - mount: scheme summary and target template (e.g., "/var/cache/{UUID}")
|
||||||
///
|
///
|
||||||
/// This function is non-destructive and performs no probing beyond the provided inputs.
|
/// This function is non-destructive and performs no probing beyond the provided inputs.
|
||||||
fn build_summary_json(disks: &[Disk], plan: &partition::PartitionPlan, cfg: &Config) -> Result<serde_json::Value> {
|
fn build_summary_json(
|
||||||
|
disks: &[Disk],
|
||||||
|
plan: &partition::PartitionPlan,
|
||||||
|
cfg: &Config,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
// Disks summary
|
// Disks summary
|
||||||
let disks_json: Vec<serde_json::Value> = disks
|
let disks_json: Vec<serde_json::Value> = disks
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
// ext: device-specific alignment or reserved areas configurable via cfg in the future.
|
// ext: device-specific alignment or reserved areas configurable via cfg in the future.
|
||||||
// REGION: EXTENSION_POINTS-END
|
// REGION: EXTENSION_POINTS-END
|
||||||
//
|
//
|
||||||
// REGION: SAFETY
|
// REGION: SAFETY
|
||||||
// safety: must verify require_empty_disks before any modification.
|
// safety: must verify require_empty_disks before any modification.
|
||||||
// safety: when UEFI-booted, suppress creating BIOS boot partition to avoid unnecessary ef02 on UEFI systems.
|
// safety: when UEFI-booted, suppress creating BIOS boot partition to avoid unnecessary ef02 on UEFI systems.
|
||||||
// safety: must ensure unique partition GUIDs; identical labels are allowed when expected (e.g., ESP ZOSBOOT).
|
// safety: must ensure unique partition GUIDs; identical labels are allowed when expected (e.g., ESP ZOSBOOT).
|
||||||
// safety: must call udev settle after partition table writes.
|
// safety: must call udev settle after partition table writes.
|
||||||
// REGION: SAFETY-END
|
// REGION: SAFETY-END
|
||||||
//
|
//
|
||||||
// REGION: ERROR_MAPPING
|
// REGION: ERROR_MAPPING
|
||||||
// errmap: external tool failure -> crate::Error::Tool { tool, status, stderr }.
|
// errmap: external tool failure -> crate::Error::Tool { tool, status, stderr }.
|
||||||
@@ -44,11 +44,11 @@
|
|||||||
//! [fn apply_partitions](plan.rs:1).
|
//! [fn apply_partitions](plan.rs:1).
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{Config, Topology},
|
|
||||||
device::Disk,
|
|
||||||
util::{run_cmd, run_cmd_capture, which_tool, udev_settle, is_efi_boot},
|
|
||||||
idempotency,
|
|
||||||
Error, Result,
|
Error, Result,
|
||||||
|
device::Disk,
|
||||||
|
idempotency,
|
||||||
|
types::{Config, Topology},
|
||||||
|
util::{is_efi_boot, run_cmd, run_cmd_capture, udev_settle, which_tool},
|
||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
@@ -117,20 +117,20 @@ pub struct PartitionResult {
|
|||||||
pub device_path: String,
|
pub device_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute GPT-only plan per topology and constraints.
|
/// Compute GPT-only plan per topology and constraints.
|
||||||
///
|
///
|
||||||
/// Layout defaults:
|
/// Layout defaults:
|
||||||
/// - BIOS boot: cfg.partitioning.bios_boot if enabled (size_mib)
|
/// - BIOS boot: cfg.partitioning.bios_boot if enabled (size_mib)
|
||||||
/// - ESP: cfg.partitioning.esp.size_mib, GPT name cfg.partitioning.esp.gpt_name (typically "zosboot")
|
/// - ESP: cfg.partitioning.esp.size_mib, GPT name cfg.partitioning.esp.gpt_name (typically "zosboot")
|
||||||
/// - Data: remainder, GPT name cfg.partitioning.data.gpt_name ("zosdata")
|
/// - Data: remainder, GPT name cfg.partitioning.data.gpt_name ("zosdata")
|
||||||
/// - Cache (only for SSD/HDD topology): remainder on SSD after boot/ESP, GPT name cfg.partitioning.cache.gpt_name ("zoscache")
|
/// - Cache (only for SSD/HDD topology): remainder on SSD after boot/ESP, GPT name cfg.partitioning.cache.gpt_name ("zoscache")
|
||||||
///
|
///
|
||||||
/// Topology mapping:
|
/// Topology mapping:
|
||||||
/// - Single: use first eligible disk; create BIOS (opt) + ESP + Data
|
/// - Single: use first eligible disk; create BIOS (opt) + ESP + Data
|
||||||
/// - DualIndependent: need at least 2 disks; disk0: BIOS (opt) + ESP + Data, disk1: Data
|
/// - DualIndependent: need at least 2 disks; disk0: BIOS (opt) + ESP + Data, disk1: Data
|
||||||
/// - BtrfsRaid1: need at least 2 disks; disk0: BIOS (opt) + ESP + Data, disk1: Data
|
/// - BtrfsRaid1: need at least 2 disks; disk0: BIOS (opt) + ESP + Data, disk1: Data
|
||||||
/// - SsdHddBcachefs: need >=1 SSD (rotational=false) and >=1 HDD (rotational=true);
|
/// - SsdHddBcachefs: need >=1 SSD (rotational=false) and >=1 HDD (rotational=true);
|
||||||
/// SSD: BIOS (opt) + ESP + Cache; HDD: Data
|
/// SSD: BIOS (opt) + ESP + Cache; HDD: Data
|
||||||
pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
||||||
let align = cfg.partitioning.alignment_mib;
|
let align = cfg.partitioning.alignment_mib;
|
||||||
let require_empty = cfg.partitioning.require_empty_disks;
|
let require_empty = cfg.partitioning.require_empty_disks;
|
||||||
@@ -138,7 +138,9 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
let add_bios = cfg.partitioning.bios_boot.enabled && !is_efi_boot();
|
let add_bios = cfg.partitioning.bios_boot.enabled && !is_efi_boot();
|
||||||
|
|
||||||
if disks.is_empty() {
|
if disks.is_empty() {
|
||||||
return Err(Error::Partition("no disks provided to partition planner".into()));
|
return Err(Error::Partition(
|
||||||
|
"no disks provided to partition planner".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut plans: Vec<DiskPlan> = Vec::new();
|
let mut plans: Vec<DiskPlan> = Vec::new();
|
||||||
@@ -164,7 +166,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d0.clone(), parts });
|
plans.push(DiskPlan {
|
||||||
|
disk: d0.clone(),
|
||||||
|
parts,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Topology::BcachefsSingle => {
|
Topology::BcachefsSingle => {
|
||||||
let d0 = &disks[0];
|
let d0 = &disks[0];
|
||||||
@@ -186,11 +191,16 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d0.clone(), parts });
|
plans.push(DiskPlan {
|
||||||
|
disk: d0.clone(),
|
||||||
|
parts,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Topology::DualIndependent => {
|
Topology::DualIndependent => {
|
||||||
if disks.len() < 2 {
|
if disks.len() < 2 {
|
||||||
return Err(Error::Partition("DualIndependent topology requires at least 2 disks".into()));
|
return Err(Error::Partition(
|
||||||
|
"DualIndependent topology requires at least 2 disks".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let d0 = &disks[0];
|
let d0 = &disks[0];
|
||||||
let d1 = &disks[1];
|
let d1 = &disks[1];
|
||||||
@@ -214,7 +224,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d0.clone(), parts: parts0 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d0.clone(),
|
||||||
|
parts: parts0,
|
||||||
|
});
|
||||||
|
|
||||||
// Disk 1: Data only
|
// Disk 1: Data only
|
||||||
let mut parts1 = Vec::new();
|
let mut parts1 = Vec::new();
|
||||||
@@ -223,11 +236,16 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d1.clone(), parts: parts1 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d1.clone(),
|
||||||
|
parts: parts1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Topology::BtrfsRaid1 => {
|
Topology::BtrfsRaid1 => {
|
||||||
if disks.len() < 2 {
|
if disks.len() < 2 {
|
||||||
return Err(Error::Partition("BtrfsRaid1 topology requires at least 2 disks".into()));
|
return Err(Error::Partition(
|
||||||
|
"BtrfsRaid1 topology requires at least 2 disks".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let d0 = &disks[0];
|
let d0 = &disks[0];
|
||||||
let d1 = &disks[1];
|
let d1 = &disks[1];
|
||||||
@@ -251,7 +269,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d0.clone(), parts: parts0 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d0.clone(),
|
||||||
|
parts: parts0,
|
||||||
|
});
|
||||||
|
|
||||||
// Disk 1: Data only (for RAID1)
|
// Disk 1: Data only (for RAID1)
|
||||||
let mut parts1 = Vec::new();
|
let mut parts1 = Vec::new();
|
||||||
@@ -260,11 +281,16 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d1.clone(), parts: parts1 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d1.clone(),
|
||||||
|
parts: parts1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Topology::Bcachefs2Copy => {
|
Topology::Bcachefs2Copy => {
|
||||||
if disks.len() < 2 {
|
if disks.len() < 2 {
|
||||||
return Err(Error::Partition("Bcachefs2Copy topology requires at least 2 disks".into()));
|
return Err(Error::Partition(
|
||||||
|
"Bcachefs2Copy topology requires at least 2 disks".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let d0 = &disks[0];
|
let d0 = &disks[0];
|
||||||
let d1 = &disks[1];
|
let d1 = &disks[1];
|
||||||
@@ -288,7 +314,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d0.clone(), parts: parts0 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d0.clone(),
|
||||||
|
parts: parts0,
|
||||||
|
});
|
||||||
|
|
||||||
// Disk 1: Data only
|
// Disk 1: Data only
|
||||||
let mut parts1 = Vec::new();
|
let mut parts1 = Vec::new();
|
||||||
@@ -297,14 +326,19 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: d1.clone(), parts: parts1 });
|
plans.push(DiskPlan {
|
||||||
|
disk: d1.clone(),
|
||||||
|
parts: parts1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Topology::SsdHddBcachefs => {
|
Topology::SsdHddBcachefs => {
|
||||||
// Choose SSD (rotational=false) and HDD (rotational=true)
|
// Choose SSD (rotational=false) and HDD (rotational=true)
|
||||||
let ssd = disks.iter().find(|d| !d.rotational)
|
let ssd = disks.iter().find(|d| !d.rotational).ok_or_else(|| {
|
||||||
.ok_or_else(|| Error::Partition("SsdHddBcachefs requires an SSD (non-rotational) disk".into()))?;
|
Error::Partition("SsdHddBcachefs requires an SSD (non-rotational) disk".into())
|
||||||
let hdd = disks.iter().find(|d| d.rotational)
|
})?;
|
||||||
.ok_or_else(|| Error::Partition("SsdHddBcachefs requires an HDD (rotational) disk".into()))?;
|
let hdd = disks.iter().find(|d| d.rotational).ok_or_else(|| {
|
||||||
|
Error::Partition("SsdHddBcachefs requires an HDD (rotational) disk".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
// SSD: BIOS (opt) + ESP + Cache remainder
|
// SSD: BIOS (opt) + ESP + Cache remainder
|
||||||
let mut parts_ssd = Vec::new();
|
let mut parts_ssd = Vec::new();
|
||||||
@@ -325,7 +359,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.cache.gpt_name.clone(),
|
gpt_name: cfg.partitioning.cache.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: ssd.clone(), parts: parts_ssd });
|
plans.push(DiskPlan {
|
||||||
|
disk: ssd.clone(),
|
||||||
|
parts: parts_ssd,
|
||||||
|
});
|
||||||
|
|
||||||
// HDD: Data remainder
|
// HDD: Data remainder
|
||||||
let mut parts_hdd = Vec::new();
|
let mut parts_hdd = Vec::new();
|
||||||
@@ -334,7 +371,10 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
|||||||
size_mib: None,
|
size_mib: None,
|
||||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||||
});
|
});
|
||||||
plans.push(DiskPlan { disk: hdd.clone(), parts: parts_hdd });
|
plans.push(DiskPlan {
|
||||||
|
disk: hdd.clone(),
|
||||||
|
parts: parts_hdd,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,10 +429,17 @@ pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
|
|||||||
if let Some(blockdev) = which_tool("blockdev")? {
|
if let Some(blockdev) = which_tool("blockdev")? {
|
||||||
let out = run_cmd_capture(&[blockdev.as_str(), "--getss", disk_path])?;
|
let out = run_cmd_capture(&[blockdev.as_str(), "--getss", disk_path])?;
|
||||||
let s = out.stdout.trim();
|
let s = out.stdout.trim();
|
||||||
return s.parse::<u64>()
|
return s.parse::<u64>().map_err(|e| {
|
||||||
.map_err(|e| Error::Partition(format!("failed to parse sector size from blockdev for {}: {}", disk_path, e)));
|
Error::Partition(format!(
|
||||||
|
"failed to parse sector size from blockdev for {}: {}",
|
||||||
|
disk_path, e
|
||||||
|
))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
warn!("blockdev not found; assuming 512-byte sectors for {}", disk_path);
|
warn!(
|
||||||
|
"blockdev not found; assuming 512-byte sectors for {}",
|
||||||
|
disk_path
|
||||||
|
);
|
||||||
Ok(512)
|
Ok(512)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,20 +457,29 @@ pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
|
|||||||
// Format: "First sector: 2048 (at 1024.0 KiB)"
|
// Format: "First sector: 2048 (at 1024.0 KiB)"
|
||||||
let val = rest.trim().split_whitespace().next().unwrap_or("");
|
let val = rest.trim().split_whitespace().next().unwrap_or("");
|
||||||
if !val.is_empty() {
|
if !val.is_empty() {
|
||||||
first = Some(val.parse::<u64>().map_err(|e| Error::Partition(format!("parse first sector: {}", e)))?);
|
first = Some(
|
||||||
|
val.parse::<u64>()
|
||||||
|
.map_err(|e| Error::Partition(format!("parse first sector: {}", e)))?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(rest) = line.strip_prefix("Last sector:") {
|
} else if let Some(rest) = line.strip_prefix("Last sector:") {
|
||||||
let val = rest.trim().split_whitespace().next().unwrap_or("");
|
let val = rest.trim().split_whitespace().next().unwrap_or("");
|
||||||
if !val.is_empty() {
|
if !val.is_empty() {
|
||||||
last = Some(val.parse::<u64>().map_err(|e| Error::Partition(format!("parse last sector: {}", e)))?);
|
last = Some(
|
||||||
|
val.parse::<u64>()
|
||||||
|
.map_err(|e| Error::Partition(format!("parse last sector: {}", e)))?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = first.ok_or_else(|| Error::Partition("sgdisk -i missing First sector".into()))?;
|
let first =
|
||||||
|
first.ok_or_else(|| Error::Partition("sgdisk -i missing First sector".into()))?;
|
||||||
let last = last.ok_or_else(|| Error::Partition("sgdisk -i missing Last sector".into()))?;
|
let last = last.ok_or_else(|| Error::Partition("sgdisk -i missing Last sector".into()))?;
|
||||||
if guid.is_empty() {
|
if guid.is_empty() {
|
||||||
return Err(Error::Partition("sgdisk -i missing Partition unique GUID".into()));
|
return Err(Error::Partition(
|
||||||
|
"sgdisk -i missing Partition unique GUID".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok((guid, first, last))
|
Ok((guid, first, last))
|
||||||
}
|
}
|
||||||
@@ -467,9 +523,12 @@ pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
|
|||||||
|
|
||||||
run_cmd(&[
|
run_cmd(&[
|
||||||
sgdisk.as_str(),
|
sgdisk.as_str(),
|
||||||
"-n", n_arg.as_str(),
|
"-n",
|
||||||
"-t", t_arg.as_str(),
|
n_arg.as_str(),
|
||||||
"-c", c_arg.as_str(),
|
"-t",
|
||||||
|
t_arg.as_str(),
|
||||||
|
"-c",
|
||||||
|
c_arg.as_str(),
|
||||||
disk_path,
|
disk_path,
|
||||||
])?;
|
])?;
|
||||||
}
|
}
|
||||||
@@ -486,11 +545,7 @@ pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
|
|||||||
|
|
||||||
// Query sgdisk for partition info
|
// Query sgdisk for partition info
|
||||||
let i_arg = format!("{}", part_num);
|
let i_arg = format!("{}", part_num);
|
||||||
let info_out = run_cmd_capture(&[
|
let info_out = run_cmd_capture(&[sgdisk.as_str(), "-i", i_arg.as_str(), disk_path])?;
|
||||||
sgdisk.as_str(),
|
|
||||||
"-i", i_arg.as_str(),
|
|
||||||
disk_path,
|
|
||||||
])?;
|
|
||||||
|
|
||||||
let (unique_guid, first_sector, last_sector) = parse_sgdisk_info(&info_out.stdout)?;
|
let (unique_guid, first_sector, last_sector) = parse_sgdisk_info(&info_out.stdout)?;
|
||||||
let sectors = if last_sector >= first_sector {
|
let sectors = if last_sector >= first_sector {
|
||||||
@@ -516,6 +571,9 @@ pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("apply_partitions: created {} partition entries", results.len());
|
debug!(
|
||||||
|
"apply_partitions: created {} partition entries",
|
||||||
|
results.len()
|
||||||
|
);
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
// - Keep field names and enums stable; update docs/SCHEMA.md when public surface changes.
|
// - Keep field names and enums stable; update docs/SCHEMA.md when public surface changes.
|
||||||
// REGION: RESPONSIBILITIES-END
|
// REGION: RESPONSIBILITIES-END
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoggingConfig {
|
pub struct LoggingConfig {
|
||||||
@@ -55,7 +55,11 @@ pub enum Topology {
|
|||||||
#[value(alias = "ssd-hdd-bcachefs")]
|
#[value(alias = "ssd-hdd-bcachefs")]
|
||||||
SsdHddBcachefs,
|
SsdHddBcachefs,
|
||||||
/// Multi-device bcachefs with two replicas (data+metadata).
|
/// Multi-device bcachefs with two replicas (data+metadata).
|
||||||
#[value(alias = "bcachefs2-copy", alias = "bcachefs-2copy", alias = "bcachefs-2-copy")]
|
#[value(
|
||||||
|
alias = "bcachefs2-copy",
|
||||||
|
alias = "bcachefs-2copy",
|
||||||
|
alias = "bcachefs-2-copy"
|
||||||
|
)]
|
||||||
Bcachefs2Copy,
|
Bcachefs2Copy,
|
||||||
/// Optional mirrored btrfs across two disks when explicitly requested.
|
/// Optional mirrored btrfs across two disks when explicitly requested.
|
||||||
#[value(alias = "btrfs-raid1")]
|
#[value(alias = "btrfs-raid1")]
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
//! and consistent error handling.
|
//! and consistent error handling.
|
||||||
|
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use std::process::Command;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Captured output from an external tool invocation.
|
/// Captured output from an external tool invocation.
|
||||||
@@ -77,9 +77,10 @@ pub fn run_cmd(args: &[&str]) -> Result<()> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
debug!(target: "util.run_cmd", "exec: {:?}", args);
|
debug!(target: "util.run_cmd", "exec: {:?}", args);
|
||||||
let output = Command::new(args[0]).args(&args[1..]).output().map_err(|e| {
|
let output = Command::new(args[0])
|
||||||
Error::Other(anyhow::anyhow!("failed to spawn {:?}: {}", args, e))
|
.args(&args[1..])
|
||||||
})?;
|
.output()
|
||||||
|
.map_err(|e| Error::Other(anyhow::anyhow!("failed to spawn {:?}: {}", args, e)))?;
|
||||||
|
|
||||||
let status_code = output.status.code().unwrap_or(-1);
|
let status_code = output.status.code().unwrap_or(-1);
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
@@ -103,9 +104,10 @@ pub fn run_cmd_capture(args: &[&str]) -> Result<CmdOutput> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
debug!(target: "util.run_cmd_capture", "exec: {:?}", args);
|
debug!(target: "util.run_cmd_capture", "exec: {:?}", args);
|
||||||
let output = Command::new(args[0]).args(&args[1..]).output().map_err(|e| {
|
let output = Command::new(args[0])
|
||||||
Error::Other(anyhow::anyhow!("failed to spawn {:?}: {}", args, e))
|
.args(&args[1..])
|
||||||
})?;
|
.output()
|
||||||
|
.map_err(|e| Error::Other(anyhow::anyhow!("failed to spawn {:?}: {}", args, e)))?;
|
||||||
let status_code = output.status.code().unwrap_or(-1);
|
let status_code = output.status.code().unwrap_or(-1);
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
|||||||
Reference in New Issue
Block a user