* read /proc/self/mountinfo to detect already mounted targets and reuse their metadata
* reject conflicting mounts by source, allowing reruns without duplicating entries
* return mount results sorted by target for deterministic downstream behavior
* write subvolume and /var/mounts/{UUID} entries to fstab when requested
503 lines
18 KiB
Rust
503 lines
18 KiB
Rust
// 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::types::Config) -> crate::Result<MountPlan>
|
|
// api: mount::apply_mounts(plan: &MountPlan) -> crate::Result<Vec<MountResult>>
|
|
// api: mount::maybe_write_fstab(mounts: &[MountResult], cfg: &crate::types::Config) -> crate::Result<()>
|
|
// REGION: API-END
|
|
//
|
|
// REGION: RESPONSIBILITIES
|
|
// - 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: SAFETY
|
|
// - 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
|
|
// - 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
|
|
// - 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.
|
|
//!
|
|
//! 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::{
|
|
fs::{FsKind, FsResult},
|
|
types::Config,
|
|
util::{run_cmd, run_cmd_capture, which_tool},
|
|
Error, Result,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::fs::{create_dir_all, File};
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use tracing::info;
|
|
|
|
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"];
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct ExistingMount {
|
|
source: String,
|
|
fstype: String,
|
|
options: String,
|
|
}
|
|
|
|
fn current_mounts() -> HashMap<String, ExistingMount> {
|
|
let mut map = HashMap::new();
|
|
if let Ok(content) = std::fs::read_to_string("/proc/self/mountinfo") {
|
|
for line in content.lines() {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 7 {
|
|
continue;
|
|
}
|
|
let target = parts[4].to_string();
|
|
let mount_options = parts[5].to_string();
|
|
if let Some(idx) = parts.iter().position(|p| *p == "-") {
|
|
if idx + 2 < parts.len() {
|
|
let fstype = parts[idx + 1].to_string();
|
|
let source = parts[idx + 2].to_string();
|
|
let super_opts = if idx + 3 < parts.len() {
|
|
parts[idx + 3].to_string()
|
|
} else {
|
|
String::new()
|
|
};
|
|
let combined_options = if super_opts.is_empty() {
|
|
mount_options.clone()
|
|
} else {
|
|
format!("{mount_options},{super_opts}")
|
|
};
|
|
map.insert(
|
|
target,
|
|
ExistingMount {
|
|
source,
|
|
fstype,
|
|
options: combined_options,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
map
|
|
}
|
|
|
|
fn source_matches_uuid(existing_source: &str, uuid: &str) -> bool {
|
|
if existing_source == format!("UUID={}", uuid) {
|
|
return true;
|
|
}
|
|
if let Some(existing_uuid) = existing_source.strip_prefix("UUID=") {
|
|
return existing_uuid == uuid;
|
|
}
|
|
if existing_source.starts_with("/dev/") {
|
|
let uuid_path = Path::new("/dev/disk/by-uuid").join(uuid);
|
|
if let (Ok(existing_canon), Ok(uuid_canon)) = (
|
|
std::fs::canonicalize(existing_source),
|
|
std::fs::canonicalize(&uuid_path),
|
|
) {
|
|
return existing_canon == uuid_canon;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
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"
|
|
}
|
|
|
|
#[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 as "UUID=..." (never device paths).
|
|
pub source: String,
|
|
/// Target directory.
|
|
pub target: String,
|
|
/// Filesystem type string.
|
|
pub fstype: String,
|
|
/// Options used for the mount.
|
|
pub options: String,
|
|
}
|
|
|
|
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> {
|
|
// 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(),
|
|
));
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
|
|
// 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_map: HashMap<String, MountResult> = HashMap::new();
|
|
let mut existing_mounts = current_mounts();
|
|
|
|
// Root mounts
|
|
for pm in &plan.root_mounts {
|
|
let source = format!("UUID={}", pm.uuid);
|
|
if let Some(existing) = existing_mounts.get(pm.target.as_str()) {
|
|
if source_matches_uuid(&existing.source, &pm.uuid) {
|
|
info!(
|
|
"mount::apply_mounts: target {} already mounted; skipping",
|
|
pm.target
|
|
);
|
|
let existing_fstype = existing.fstype.clone();
|
|
let existing_options = existing.options.clone();
|
|
results_map
|
|
.entry(pm.target.clone())
|
|
.or_insert_with(|| MountResult {
|
|
source: source.clone(),
|
|
target: pm.target.clone(),
|
|
fstype: existing_fstype,
|
|
options: existing_options,
|
|
});
|
|
continue;
|
|
} else {
|
|
return Err(Error::Mount(format!(
|
|
"target {} already mounted by {} (expected UUID={})",
|
|
pm.target, existing.source, 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)?;
|
|
existing_mounts.insert(
|
|
pm.target.clone(),
|
|
ExistingMount {
|
|
source: source.clone(),
|
|
fstype: pm.fstype.clone(),
|
|
options: pm.options.clone(),
|
|
},
|
|
);
|
|
results_map.insert(
|
|
pm.target.clone(),
|
|
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);
|
|
if let Some(existing) = existing_mounts.get(sm.target.as_str()) {
|
|
if source_matches_uuid(&existing.source, &sm.uuid) {
|
|
info!(
|
|
"mount::apply_mounts: target {} already mounted; skipping",
|
|
sm.target
|
|
);
|
|
let existing_fstype = existing.fstype.clone();
|
|
let existing_options = existing.options.clone();
|
|
results_map
|
|
.entry(sm.target.clone())
|
|
.or_insert_with(|| MountResult {
|
|
source: source.clone(),
|
|
target: sm.target.clone(),
|
|
fstype: existing_fstype,
|
|
options: existing_options,
|
|
});
|
|
continue;
|
|
} else {
|
|
return Err(Error::Mount(format!(
|
|
"target {} already mounted by {} (expected UUID={})",
|
|
sm.target, existing.source, 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)?;
|
|
existing_mounts.insert(
|
|
sm.target.clone(),
|
|
ExistingMount {
|
|
source: source.clone(),
|
|
fstype: sm.fstype.clone(),
|
|
options: sm.options.clone(),
|
|
},
|
|
);
|
|
results_map.insert(
|
|
sm.target.clone(),
|
|
MountResult {
|
|
source,
|
|
target: sm.target.clone(),
|
|
fstype: sm.fstype.clone(),
|
|
options: sm.options.clone(),
|
|
},
|
|
);
|
|
}
|
|
|
|
let mut results: Vec<MountResult> = results_map.into_values().collect();
|
|
results.sort_by(|a, b| a.target.cmp(&b.target));
|
|
|
|
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(());
|
|
}
|
|
|
|
// Partition mount results into runtime root mounts and final subvolume targets.
|
|
let mut root_entries: Vec<&MountResult> = mounts
|
|
.iter()
|
|
.filter(|m| m.target.starts_with(ROOT_BASE))
|
|
.collect();
|
|
let wanted = [TARGET_ETC, TARGET_MODULES, TARGET_SYSTEM, TARGET_VM_META];
|
|
let mut subvol_entries: Vec<&MountResult> = mounts
|
|
.iter()
|
|
.filter(|m| wanted.contains(&m.target.as_str()))
|
|
.collect();
|
|
|
|
// Sort by target path ascending to be deterministic (roots before subvols).
|
|
root_entries.sort_by(|a, b| a.target.cmp(&b.target));
|
|
subvol_entries.sort_by(|a, b| a.target.cmp(&b.target));
|
|
|
|
// Compose lines: include all root mounts first, followed by the four subvol targets.
|
|
let mut lines: Vec<String> = Vec::new();
|
|
for m in root_entries.into_iter().chain(subvol_entries.into_iter()) {
|
|
// 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(())
|
|
} |