mount: use bcachefs -o X-mount.subdir={name} for subvolume mounts; update SAFETY notes; sync README and PROMPT with root/subvol scheme and bcachefs option
This commit is contained in:
17
PROMPT.md
17
PROMPT.md
@@ -30,11 +30,22 @@ Partitioning Requirements
|
||||
- Before making changes, verify the device has no existing partitions or filesystem signatures; abort otherwise.
|
||||
|
||||
Filesystem Provisioning
|
||||
- All data mounts are placed somewhere under `/var/cache`. Precise mountpoints and subvolume strategies are configurable.
|
||||
- Mount scheme and subvolumes:
|
||||
* Root mounts for each data filesystem at `/var/mounts/{UUID}` (runtime only). For btrfs root, use `-o subvolid=5`; for bcachefs root, no subdir option.
|
||||
* Create or ensure subvolumes on the primary data filesystem with names: `system`, `etc`, `modules`, `vm-meta`.
|
||||
* Mount subvolumes to final targets:
|
||||
- `/var/cache/system`
|
||||
- `/var/cache/etc`
|
||||
- `/var/cache/modules`
|
||||
- `/var/cache/vm-meta`
|
||||
* Use UUID= sources for all mounts (never device paths).
|
||||
* Subvolume options:
|
||||
- btrfs: `-o subvol={name},noatime`
|
||||
- bcachefs: `-o X-mount.subdir={name},noatime`
|
||||
- Supported backends:
|
||||
* Single disk: default to `btrfs`, label `ZOSDATA`.
|
||||
* Two disks/NVMe: default to individual `btrfs` filesystems per disk, each labeled `ZOSDATA`, mounted under `/var/cache/<UUID>` (exact path pattern TBD). Optional support for `btrfs` RAID1 or `bcachefs` RAID1 if requested.
|
||||
* Mixed SSD/NVMe + HDD: default to `bcachefs` with SSD as cache/promote and HDD as backing store, label resulting filesystem `ZOSDATA`. Alternative mode: separate `btrfs` per device (label `ZOSDATA`).
|
||||
* Two disks/NVMe (dual_independent): default to independent `btrfs` per disk, each labeled `ZOSDATA`; root-mount all under `/var/mounts/{UUID}`, pick the first data FS as primary for final subvol mounts.
|
||||
* Mixed SSD/NVMe + HDD: default to `bcachefs` with SSD as cache/promote and HDD as backing store, resulting FS labeled `ZOSDATA`. Alternative mode: separate `btrfs` per device (label `ZOSDATA`).
|
||||
- Reserved filesystem labels: `ZOSBOOT` (ESP), `ZOSDATA` (all data filesystems). GPT partition names: `zosboot` (bios_boot and ESP), `zosdata` (data), `zoscache` (cache).
|
||||
- Filesystem tuning options (compression, RAID profile, etc.) must be configurable; define sensible defaults and provide extension points.
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -135,8 +135,18 @@ Defaults and policies
|
||||
btrfs (data) label: ZOSDATA
|
||||
bcachefs (data/cache) label: ZOSDATA
|
||||
- Mount scheme:
|
||||
per-UUID under /var/cache/{UUID}
|
||||
/etc/fstab generation is disabled by default
|
||||
- Root mounts (runtime only): each data filesystem is mounted at /var/mounts/{UUID}
|
||||
- btrfs root options: rw,noatime,subvolid=5
|
||||
- bcachefs root options: rw,noatime
|
||||
- Subvolume mounts (from the primary data filesystem only) to final targets:
|
||||
- /var/cache/system
|
||||
- /var/cache/etc
|
||||
- /var/cache/modules
|
||||
- /var/cache/vm-meta
|
||||
- Subvolume mount options:
|
||||
- btrfs: -o rw,noatime,subvol={name}
|
||||
- bcachefs: -o rw,noatime,X-mount.subdir={name}
|
||||
- /etc/fstab generation is disabled by default; when enabled, only the four subvolume mounts are written (UUID= sources, deterministic order)
|
||||
|
||||
Tracing and logs
|
||||
- stderr logging level controlled by -l/--log-level (info by default)
|
||||
|
||||
364
src/mount/ops.rs
364
src/mount/ops.rs
@@ -1,85 +1,355 @@
|
||||
// REGION: API
|
||||
// api: mount::MountPlan { entries: Vec<(String, String, String, String)> }
|
||||
// note: tuple order = (source, target, fstype, options)
|
||||
// REGION: API — one-liners for plan_mounts/apply_mounts/maybe_write_fstab and structs
|
||||
// api: mount::MountPlan { root_mounts: Vec<PlannedMount>, subvol_mounts: Vec<PlannedSubvolMount>, primary_uuid: Option<String> }
|
||||
// api: mount::MountResult { source: String, target: String, fstype: String, options: String }
|
||||
// api: mount::plan_mounts(fs_results: &[crate::fs::FsResult], cfg: &crate::config::types::Config) -> crate::Result<MountPlan>
|
||||
// api: mount::plan_mounts(fs_results: &[crate::fs::FsResult], cfg: &crate::types::Config) -> crate::Result<MountPlan>
|
||||
// api: mount::apply_mounts(plan: &MountPlan) -> crate::Result<Vec<MountResult>>
|
||||
// api: mount::maybe_write_fstab(mounts: &[MountResult], cfg: &crate::config::types::Config) -> crate::Result<()>
|
||||
// api: mount::maybe_write_fstab(mounts: &[MountResult], cfg: &crate::types::Config) -> crate::Result<()>
|
||||
// REGION: API-END
|
||||
//
|
||||
// REGION: RESPONSIBILITIES
|
||||
// - Translate filesystem identities to mount targets, defaulting to /var/cache/<UUID>.
|
||||
// - Perform mounts using syscalls (nix) and create target directories as needed.
|
||||
// - Optionally generate /etc/fstab entries in deterministic order.
|
||||
// Non-goals: filesystem creation, device discovery, partitioning.
|
||||
// - Implement mount phase only: plan root mounts under /var/mounts/{UUID}, ensure/plan subvols, and mount subvols to /var/cache/*.
|
||||
// - Use UUID= sources, deterministic primary selection (first FsResult) for dual_independent.
|
||||
// - Generate fstab entries only for four subvol targets; exclude runtime root mounts.
|
||||
// REGION: RESPONSIBILITIES-END
|
||||
//
|
||||
// REGION: EXTENSION_POINTS
|
||||
// ext: support custom mount scheme mapping beyond per-UUID.
|
||||
// ext: add configurable mount options per filesystem kind via Config.
|
||||
// REGION: EXTENSION_POINTS-END
|
||||
//
|
||||
// REGION: SAFETY
|
||||
// safety: must ensure target directories exist and avoid overwriting unintended paths.
|
||||
// safety: ensure options include sensible defaults (e.g., btrfs compress, ssd) when applicable.
|
||||
// - Never mount ESP; only Btrfs/Bcachefs data FS. Root btrfs mounts use subvolid=5 (top-level).
|
||||
// - Create-if-missing subvolumes prior to subvol mounts; ensure directories exist.
|
||||
// - Always use UUID= sources; no device paths.
|
||||
// - Bcachefs subvolume mounts use option key 'X-mount.subdir={name}' (not 'subvol=').
|
||||
// REGION: SAFETY-END
|
||||
//
|
||||
// REGION: ERROR_MAPPING
|
||||
// errmap: syscall failures -> crate::Error::Mount with context.
|
||||
// errmap: fstab write IO errors -> crate::Error::Mount with path details.
|
||||
// - External tool failures map to Error::Tool via util::run_cmd/run_cmd_capture.
|
||||
// - Missing required tools map to Error::Mount with clear explanation.
|
||||
// REGION: ERROR_MAPPING-END
|
||||
//
|
||||
// REGION: TODO
|
||||
// todo: implement option synthesis (e.g., compress=zstd:3 for btrfs) based on Config and device rotational hints.
|
||||
// todo: implement deterministic fstab ordering and idempotent writes.
|
||||
// - Defer compression/SSD options; later map from Config into mount options.
|
||||
// - Consider validating tool presence up-front for clearer early errors.
|
||||
// REGION: TODO-END
|
||||
//! Mount planning and application.
|
||||
//!
|
||||
//! Translates filesystem results into mount targets (default under /var/cache/<UUID>)
|
||||
//! and applies mounts using syscalls (via nix) in later implementation.
|
||||
//!
|
||||
//! See [fn plan_mounts](ops.rs:1), [fn apply_mounts](ops.rs:1),
|
||||
//! and [fn maybe_write_fstab](ops.rs:1).
|
||||
//! See [fn plan_mounts()](src/mount/ops.rs:1), [fn apply_mounts()](src/mount/ops.rs:1),
|
||||
//! and [fn maybe_write_fstab()](src/mount/ops.rs:1).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::{Result, types::Config, fs::FsResult};
|
||||
use crate::{
|
||||
fs::{FsKind, FsResult},
|
||||
types::Config,
|
||||
util::{run_cmd, run_cmd_capture, which_tool},
|
||||
Error, Result,
|
||||
};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
const ROOT_BASE: &str = "/var/mounts";
|
||||
const TARGET_SYSTEM: &str = "/var/cache/system";
|
||||
const TARGET_ETC: &str = "/var/cache/etc";
|
||||
const TARGET_MODULES: &str = "/var/cache/modules";
|
||||
const TARGET_VM_META: &str = "/var/cache/vm-meta";
|
||||
const SUBVOLS: &[&str] = &["system", "etc", "modules", "vm-meta"];
|
||||
|
||||
/// Mount plan entries: (source, target, fstype, options)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MountPlan {
|
||||
/// Source device path, target directory, filesystem type, and mount options.
|
||||
pub entries: Vec<(String, String, String, String)>,
|
||||
pub struct PlannedMount {
|
||||
pub uuid: String, // UUID string without prefix
|
||||
pub target: String, // absolute path
|
||||
pub fstype: String, // "btrfs" | "bcachefs"
|
||||
pub options: String, // e.g., "rw,noatime,subvolid=5"
|
||||
}
|
||||
|
||||
/// Result of applying a single mount entry.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlannedSubvolMount {
|
||||
pub uuid: String, // UUID of primary FS
|
||||
pub name: String, // subvol name (system/etc/modules/vm-meta)
|
||||
pub target: String, // absolute final target
|
||||
pub fstype: String, // "btrfs" | "bcachefs"
|
||||
pub options: String, // e.g., "rw,noatime,subvol=system"
|
||||
}
|
||||
|
||||
/// Mount plan per policy.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MountPlan {
|
||||
/// Root mounts under /var/mounts/{UUID} for all data filesystems.
|
||||
pub root_mounts: Vec<PlannedMount>,
|
||||
/// Four subvol mounts chosen from the primary FS only.
|
||||
pub subvol_mounts: Vec<PlannedSubvolMount>,
|
||||
/// Primary UUID selection (only data FS; for multiple pick first in input order).
|
||||
pub primary_uuid: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of applying a mount (root or subvol).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MountResult {
|
||||
/// Source device path (e.g., /dev/nvme0n1p3).
|
||||
/// Source as "UUID=..." (never device paths).
|
||||
pub source: String,
|
||||
/// Target directory (e.g., /var/cache/<UUID>).
|
||||
/// Target directory.
|
||||
pub target: String,
|
||||
/// Filesystem type (e.g., "btrfs", "vfat").
|
||||
/// Filesystem type string.
|
||||
pub fstype: String,
|
||||
/// Options string (comma-separated).
|
||||
/// Options used for the mount.
|
||||
pub options: String,
|
||||
}
|
||||
|
||||
/// Build mount plan under /var/cache/<UUID> by default.
|
||||
fn fstype_str(kind: FsKind) -> &'static str {
|
||||
match kind {
|
||||
FsKind::Btrfs => "btrfs",
|
||||
FsKind::Bcachefs => "bcachefs",
|
||||
FsKind::Vfat => "vfat",
|
||||
}
|
||||
}
|
||||
|
||||
/// Build mount plan per policy.
|
||||
pub fn plan_mounts(fs_results: &[FsResult], _cfg: &Config) -> Result<MountPlan> {
|
||||
let _ = fs_results;
|
||||
// Placeholder: map filesystem UUIDs to per-UUID directories and assemble options.
|
||||
todo!("create per-UUID directories and mount mapping based on config")
|
||||
// Identify data filesystems (Btrfs/Bcachefs), ignore ESP (Vfat)
|
||||
let data: Vec<&FsResult> = fs_results
|
||||
.iter()
|
||||
.filter(|r| matches!(r.kind, FsKind::Btrfs | FsKind::Bcachefs))
|
||||
.collect();
|
||||
|
||||
if data.is_empty() {
|
||||
return Err(Error::Mount(
|
||||
"no data filesystems to mount (expected Btrfs or Bcachefs)".into(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Apply mounts using syscalls (nix), ensuring directories exist.
|
||||
pub fn apply_mounts(_plan: &MountPlan) -> Result<Vec<MountResult>> {
|
||||
// Placeholder: perform mount syscalls and return results.
|
||||
todo!("perform mount syscalls and return results")
|
||||
// Root mounts for all data filesystems
|
||||
let mut root_mounts: Vec<PlannedMount> = Vec::new();
|
||||
for r in &data {
|
||||
let uuid = r.uuid.clone();
|
||||
let fstype = fstype_str(r.kind).to_string();
|
||||
let target = format!("{}/{}", ROOT_BASE, uuid);
|
||||
let options = match r.kind {
|
||||
FsKind::Btrfs => "rw,noatime,subvolid=5".to_string(),
|
||||
FsKind::Bcachefs => "rw,noatime".to_string(),
|
||||
FsKind::Vfat => continue,
|
||||
};
|
||||
root_mounts.push(PlannedMount {
|
||||
uuid,
|
||||
target,
|
||||
fstype,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
/// Optionally generate /etc/fstab entries in deterministic order.
|
||||
pub fn maybe_write_fstab(_mounts: &[MountResult], _cfg: &Config) -> Result<()> {
|
||||
// Placeholder: write fstab when enabled in configuration.
|
||||
todo!("when enabled, write fstab entries deterministically")
|
||||
// Determine primary UUID
|
||||
let primary_uuid = Some(data[0].uuid.clone());
|
||||
|
||||
// Subvol mounts only from primary FS
|
||||
let primary = data[0];
|
||||
let mut subvol_mounts: Vec<PlannedSubvolMount> = Vec::new();
|
||||
let fstype = fstype_str(primary.kind).to_string();
|
||||
// Option key differs per filesystem: btrfs uses subvol=, bcachefs uses X-mount.subdir=
|
||||
let opt_key = match primary.kind {
|
||||
FsKind::Btrfs => "subvol=",
|
||||
FsKind::Bcachefs => "X-mount.subdir=",
|
||||
FsKind::Vfat => "subvol=", // not used for Vfat (ESP ignored)
|
||||
};
|
||||
for name in SUBVOLS {
|
||||
let target = match *name {
|
||||
"system" => TARGET_SYSTEM.to_string(),
|
||||
"etc" => TARGET_ETC.to_string(),
|
||||
"modules" => TARGET_MODULES.to_string(),
|
||||
"vm-meta" => TARGET_VM_META.to_string(),
|
||||
_ => continue,
|
||||
};
|
||||
let options = format!("rw,noatime,{}{}", opt_key, name);
|
||||
subvol_mounts.push(PlannedSubvolMount {
|
||||
uuid: primary.uuid.clone(),
|
||||
name: name.to_string(),
|
||||
target,
|
||||
fstype: fstype.clone(),
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(MountPlan {
|
||||
root_mounts,
|
||||
subvol_mounts,
|
||||
primary_uuid,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply mounts: ensure dirs, mount roots, create subvols if missing, mount subvols.
|
||||
pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
||||
// Tool discovery
|
||||
let mount_tool = which_tool("mount")?
|
||||
.ok_or_else(|| Error::Mount("required tool 'mount' not found in PATH".into()))?;
|
||||
|
||||
// Ensure target directories exist for root mounts
|
||||
for pm in &plan.root_mounts {
|
||||
create_dir_all(&pm.target)
|
||||
.map_err(|e| Error::Mount(format!("failed to create dir {}: {}", pm.target, e)))?;
|
||||
}
|
||||
// Ensure final subvol targets exist
|
||||
for sm in &plan.subvol_mounts {
|
||||
create_dir_all(&sm.target)
|
||||
.map_err(|e| Error::Mount(format!("failed to create dir {}: {}", sm.target, e)))?;
|
||||
}
|
||||
|
||||
let mut results: Vec<MountResult> = Vec::new();
|
||||
|
||||
// Root mounts
|
||||
for pm in &plan.root_mounts {
|
||||
let source = format!("UUID={}", pm.uuid);
|
||||
let args = [
|
||||
mount_tool.as_str(),
|
||||
"-t",
|
||||
pm.fstype.as_str(),
|
||||
"-o",
|
||||
pm.options.as_str(),
|
||||
source.as_str(),
|
||||
pm.target.as_str(),
|
||||
];
|
||||
run_cmd(&args)?;
|
||||
results.push(MountResult {
|
||||
source,
|
||||
target: pm.target.clone(),
|
||||
fstype: pm.fstype.clone(),
|
||||
options: pm.options.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Subvolume creation (create-if-missing) and mounts for the primary
|
||||
if let Some(primary_uuid) = &plan.primary_uuid {
|
||||
// Determine primary fs kind from planned subvols (they all share fstype for primary)
|
||||
let primary_kind = plan
|
||||
.subvol_mounts
|
||||
.get(0)
|
||||
.map(|s| s.fstype.clone())
|
||||
.unwrap_or_else(|| "btrfs".to_string());
|
||||
let root = format!("{}/{}", ROOT_BASE, primary_uuid);
|
||||
|
||||
if primary_kind == "btrfs" {
|
||||
let btrfs_tool = which_tool("btrfs")?
|
||||
.ok_or_else(|| Error::Mount("required tool 'btrfs' not found in PATH".into()))?;
|
||||
// List existing subvols under root
|
||||
let out = run_cmd_capture(&[
|
||||
btrfs_tool.as_str(),
|
||||
"subvolume",
|
||||
"list",
|
||||
"-o",
|
||||
root.as_str(),
|
||||
])?;
|
||||
for sm in &plan.subvol_mounts {
|
||||
if &sm.uuid != primary_uuid {
|
||||
continue;
|
||||
}
|
||||
// Check existence by scanning output for " path {name}"
|
||||
let exists = out
|
||||
.stdout
|
||||
.lines()
|
||||
.any(|l| l.contains(&format!(" path {}", sm.name)));
|
||||
if !exists {
|
||||
// Create subvolume
|
||||
let subvol_path = format!("{}/{}", root, sm.name);
|
||||
let args = [btrfs_tool.as_str(), "subvolume", "create", subvol_path.as_str()];
|
||||
run_cmd(&args)?;
|
||||
}
|
||||
}
|
||||
} else if primary_kind == "bcachefs" {
|
||||
let bcachefs_tool = which_tool("bcachefs")?.ok_or_else(|| {
|
||||
Error::Mount("required tool 'bcachefs' not found in PATH".into())
|
||||
})?;
|
||||
for sm in &plan.subvol_mounts {
|
||||
if &sm.uuid != primary_uuid {
|
||||
continue;
|
||||
}
|
||||
let subvol_path = format!("{}/{}", root, sm.name);
|
||||
if !Path::new(&subvol_path).exists() {
|
||||
let args = [
|
||||
bcachefs_tool.as_str(),
|
||||
"subvolume",
|
||||
"create",
|
||||
subvol_path.as_str(),
|
||||
];
|
||||
run_cmd(&args)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::Mount(format!(
|
||||
"unsupported primary fstype for subvols: {}",
|
||||
primary_kind
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Subvol mounts
|
||||
for sm in &plan.subvol_mounts {
|
||||
let source = format!("UUID={}", sm.uuid);
|
||||
let args = [
|
||||
mount_tool.as_str(),
|
||||
"-t",
|
||||
sm.fstype.as_str(),
|
||||
"-o",
|
||||
sm.options.as_str(),
|
||||
source.as_str(),
|
||||
sm.target.as_str(),
|
||||
];
|
||||
run_cmd(&args)?;
|
||||
results.push(MountResult {
|
||||
source,
|
||||
target: sm.target.clone(),
|
||||
fstype: sm.fstype.clone(),
|
||||
options: sm.options.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Optionally write fstab entries for subvol mounts only (deterministic order).
|
||||
pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> {
|
||||
if !cfg.mount.fstab_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Filter only the four subvol targets
|
||||
let wanted = [TARGET_ETC, TARGET_MODULES, TARGET_SYSTEM, TARGET_VM_META];
|
||||
let mut entries: Vec<&MountResult> = mounts
|
||||
.iter()
|
||||
.filter(|m| wanted.contains(&m.target.as_str()))
|
||||
.collect();
|
||||
|
||||
// Sort by target path ascending to be deterministic
|
||||
entries.sort_by(|a, b| a.target.cmp(&b.target));
|
||||
|
||||
// Compose lines
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
for m in entries {
|
||||
// m.source already "UUID=..."
|
||||
let line = format!(
|
||||
"{} {} {} {} 0 0",
|
||||
m.source, m.target, m.fstype, m.options
|
||||
);
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
// Atomic write to /etc/fstab
|
||||
let fstab_path = "/etc/fstab";
|
||||
let tmp_path = "/etc/fstab.zosstorage.tmp";
|
||||
if let Some(parent) = Path::new(fstab_path).parent() {
|
||||
create_dir_all(parent)
|
||||
.map_err(|e| Error::Mount(format!("failed to create {}: {}", parent.display(), e)))?;
|
||||
}
|
||||
{
|
||||
let mut f = File::create(tmp_path)
|
||||
.map_err(|e| Error::Mount(format!("failed to create {}: {}", tmp_path, e)))?;
|
||||
for line in lines {
|
||||
writeln!(f, "{}", line)
|
||||
.map_err(|e| Error::Mount(format!("failed to write tmp fstab: {}", e)))?;
|
||||
}
|
||||
f.flush()
|
||||
.map_err(|e| Error::Mount(format!("failed to flush tmp fstab: {}", e)))?;
|
||||
}
|
||||
std::fs::rename(tmp_path, fstab_path).map_err(|e| {
|
||||
Error::Mount(format!(
|
||||
"failed to replace {} atomically: {}",
|
||||
fstab_path, e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -201,7 +201,10 @@ pub fn run(ctx: &Context) -> Result<()> {
|
||||
let fs_results = zfs::make_filesystems(&fs_plan)?;
|
||||
info!("orchestrator: created {} filesystem(s)", fs_results.len());
|
||||
|
||||
// Next steps (mounts, optional fstab, state report) will be wired in follow-ups.
|
||||
// Mount planning and application
|
||||
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)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user