topology: add bcachefs-2copy; add bcachefs-single; rename single->btrfs-single; update planner, fs mapping, CLI, defaults, preview topo strings, README
This commit is contained in:
@@ -55,18 +55,22 @@ impl std::fmt::Display for LogLevelArg {
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[value(rename_all = "kebab_case")]
|
||||
pub enum TopologyArg {
|
||||
Single,
|
||||
BtrfsSingle,
|
||||
BcachefsSingle,
|
||||
DualIndependent,
|
||||
SsdHddBcachefs,
|
||||
Bcachefs2Copy,
|
||||
BtrfsRaid1,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TopologyArg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
TopologyArg::Single => "single",
|
||||
TopologyArg::BtrfsSingle => "btrfs_single",
|
||||
TopologyArg::BcachefsSingle => "bcachefs_single",
|
||||
TopologyArg::DualIndependent => "dual_independent",
|
||||
TopologyArg::SsdHddBcachefs => "ssd_hdd_bcachefs",
|
||||
TopologyArg::Bcachefs2Copy => "bcachefs_2copy",
|
||||
TopologyArg::BtrfsRaid1 => "btrfs_raid1",
|
||||
};
|
||||
f.write_str(s)
|
||||
|
||||
@@ -187,9 +187,11 @@ pub fn validate(cfg: &Config) -> Result<()> {
|
||||
|
||||
// Topology-specific quick checks (basic for now)
|
||||
match cfg.topology {
|
||||
Topology::Single => {} // nothing special
|
||||
Topology::BtrfsSingle => {} // nothing special
|
||||
Topology::BcachefsSingle => {}
|
||||
Topology::DualIndependent => {}
|
||||
Topology::SsdHddBcachefs => {}
|
||||
Topology::Bcachefs2Copy => {}
|
||||
Topology::BtrfsRaid1 => {
|
||||
// No enforced requirement here beyond presence of two disks at runtime.
|
||||
if cfg.filesystem.btrfs.raid_profile != "raid1" && cfg.filesystem.btrfs.raid_profile != "none" {
|
||||
@@ -352,7 +354,7 @@ fn default_config() -> Config {
|
||||
allow_removable: false,
|
||||
min_size_gib: 10,
|
||||
},
|
||||
topology: Topology::Single,
|
||||
topology: Topology::BtrfsSingle,
|
||||
partitioning: Partitioning {
|
||||
alignment_mib: 1,
|
||||
require_empty_disks: true,
|
||||
|
||||
@@ -152,8 +152,36 @@ pub fn plan_filesystems(
|
||||
label: cfg.filesystem.btrfs.label.clone(),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Map each Data partition to individual Btrfs filesystems.
|
||||
Topology::Bcachefs2Copy => {
|
||||
// Group all Data partitions into a single Bcachefs filesystem across multiple devices (2-copy semantics).
|
||||
let data_devs: Vec<String> = parts
|
||||
.iter()
|
||||
.filter(|p| matches!(p.role, PartRole::Data))
|
||||
.map(|p| p.device_path.clone())
|
||||
.collect();
|
||||
if data_devs.len() < 2 {
|
||||
return Err(Error::Filesystem(
|
||||
"Bcachefs2Copy topology requires at least 2 data partitions".to_string(),
|
||||
));
|
||||
}
|
||||
specs.push(FsSpec {
|
||||
kind: FsKind::Bcachefs,
|
||||
devices: data_devs,
|
||||
label: cfg.filesystem.bcachefs.label.clone(),
|
||||
});
|
||||
}
|
||||
Topology::BcachefsSingle => {
|
||||
// Single-device bcachefs on the sole Data partition.
|
||||
let data = parts.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 {
|
||||
kind: FsKind::Bcachefs,
|
||||
devices: vec![data.device_path.clone()],
|
||||
label: cfg.filesystem.bcachefs.label.clone(),
|
||||
});
|
||||
}
|
||||
Topology::BtrfsSingle | Topology::DualIndependent => {
|
||||
// Map Data partition(s) to Btrfs (single device per partition for DualIndependent).
|
||||
for p in parts.iter().filter(|p| matches!(p.role, PartRole::Data)) {
|
||||
specs.push(FsSpec {
|
||||
kind: FsKind::Btrfs,
|
||||
@@ -258,12 +286,11 @@ pub fn make_filesystems(plan: &FsPlan) -> Result<Vec<FsResult>> {
|
||||
let Some(ref mkfs) = bcachefs_tool else {
|
||||
return Err(Error::Filesystem("bcachefs not found in PATH".into()));
|
||||
};
|
||||
if spec.devices.len() < 2 {
|
||||
return Err(Error::Filesystem("bcachefs requires at least two devices (cache + backing)".into()));
|
||||
if spec.devices.is_empty() {
|
||||
return Err(Error::Filesystem("bcachefs requires at least one device".into()));
|
||||
}
|
||||
// bcachefs format --label LABEL dev_cache dev_backing ...
|
||||
// TODO(fs): map compression/checksum/cache-mode flags from config in a follow-up.
|
||||
// This is deferred per current scope to focus on btrfs RAID profile wiring.
|
||||
// bcachefs format --label LABEL dev_cache dev_backing ... (single-device also supported)
|
||||
// TODO(fs): map compression/checksum/cache-mode and data/metadata replica flags in a follow-up.
|
||||
let mut args: Vec<String> = vec![mkfs.clone(), "format".into(), "--label".into(), spec.label.clone()];
|
||||
args.extend(spec.devices.iter().cloned());
|
||||
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
@@ -341,9 +341,11 @@ fn build_summary_json(disks: &[Disk], plan: &partition::PartitionPlan, cfg: &Con
|
||||
|
||||
// Decide filesystem kinds and planned mountpoints (template) from plan + cfg.topology
|
||||
let topo_str = match cfg.topology {
|
||||
crate::types::Topology::Single => "single",
|
||||
crate::types::Topology::BtrfsSingle => "btrfs_single",
|
||||
crate::types::Topology::BcachefsSingle => "bcachefs_single",
|
||||
crate::types::Topology::DualIndependent => "dual_independent",
|
||||
crate::types::Topology::SsdHddBcachefs => "ssd_hdd_bcachefs",
|
||||
crate::types::Topology::Bcachefs2Copy => "bcachefs_2copy",
|
||||
crate::types::Topology::BtrfsRaid1 => "btrfs_raid1",
|
||||
};
|
||||
|
||||
|
||||
@@ -144,7 +144,29 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
||||
let mut plans: Vec<DiskPlan> = Vec::new();
|
||||
|
||||
match cfg.topology {
|
||||
Topology::Single => {
|
||||
Topology::BtrfsSingle => {
|
||||
let d0 = &disks[0];
|
||||
let mut parts = Vec::new();
|
||||
if add_bios {
|
||||
parts.push(PartitionSpec {
|
||||
role: PartRole::BiosBoot,
|
||||
size_mib: Some(cfg.partitioning.bios_boot.size_mib),
|
||||
gpt_name: cfg.partitioning.bios_boot.gpt_name.clone(),
|
||||
});
|
||||
}
|
||||
parts.push(PartitionSpec {
|
||||
role: PartRole::Esp,
|
||||
size_mib: Some(cfg.partitioning.esp.size_mib),
|
||||
gpt_name: cfg.partitioning.esp.gpt_name.clone(),
|
||||
});
|
||||
parts.push(PartitionSpec {
|
||||
role: PartRole::Data,
|
||||
size_mib: None,
|
||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||
});
|
||||
plans.push(DiskPlan { disk: d0.clone(), parts });
|
||||
}
|
||||
Topology::BcachefsSingle => {
|
||||
let d0 = &disks[0];
|
||||
let mut parts = Vec::new();
|
||||
if add_bios {
|
||||
@@ -240,6 +262,43 @@ pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
|
||||
});
|
||||
plans.push(DiskPlan { disk: d1.clone(), parts: parts1 });
|
||||
}
|
||||
Topology::Bcachefs2Copy => {
|
||||
if disks.len() < 2 {
|
||||
return Err(Error::Partition("Bcachefs2Copy topology requires at least 2 disks".into()));
|
||||
}
|
||||
let d0 = &disks[0];
|
||||
let d1 = &disks[1];
|
||||
|
||||
// Disk 0: BIOS (opt) + ESP + Data
|
||||
let mut parts0 = Vec::new();
|
||||
if add_bios {
|
||||
parts0.push(PartitionSpec {
|
||||
role: PartRole::BiosBoot,
|
||||
size_mib: Some(cfg.partitioning.bios_boot.size_mib),
|
||||
gpt_name: cfg.partitioning.bios_boot.gpt_name.clone(),
|
||||
});
|
||||
}
|
||||
parts0.push(PartitionSpec {
|
||||
role: PartRole::Esp,
|
||||
size_mib: Some(cfg.partitioning.esp.size_mib),
|
||||
gpt_name: cfg.partitioning.esp.gpt_name.clone(),
|
||||
});
|
||||
parts0.push(PartitionSpec {
|
||||
role: PartRole::Data,
|
||||
size_mib: None,
|
||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||
});
|
||||
plans.push(DiskPlan { disk: d0.clone(), parts: parts0 });
|
||||
|
||||
// Disk 1: Data only
|
||||
let mut parts1 = Vec::new();
|
||||
parts1.push(PartitionSpec {
|
||||
role: PartRole::Data,
|
||||
size_mib: None,
|
||||
gpt_name: cfg.partitioning.data.gpt_name.clone(),
|
||||
});
|
||||
plans.push(DiskPlan { disk: d1.clone(), parts: parts1 });
|
||||
}
|
||||
Topology::SsdHddBcachefs => {
|
||||
// Choose SSD (rotational=false) and HDD (rotational=true)
|
||||
let ssd = disks.iter().find(|d| !d.rotational)
|
||||
|
||||
@@ -29,11 +29,15 @@ pub struct DeviceSelection {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Topology {
|
||||
/// Single eligible disk; btrfs on remainder.
|
||||
Single,
|
||||
BtrfsSingle,
|
||||
/// Single eligible disk; bcachefs on remainder.
|
||||
BcachefsSingle,
|
||||
/// Two eligible disks; independent btrfs on each data partition.
|
||||
DualIndependent,
|
||||
/// SSD + HDD; bcachefs with SSD cache/promote and HDD backing.
|
||||
SsdHddBcachefs,
|
||||
/// Two-disk bcachefs layout using both data partitions (2 copies semantics).
|
||||
Bcachefs2Copy,
|
||||
/// Optional mirrored btrfs across two disks when explicitly requested.
|
||||
BtrfsRaid1,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user