mount: ensure idempotent results and persist runtime roots
* 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
This commit is contained in:
170
src/mount/ops.rs
170
src/mount/ops.rs
@@ -41,9 +41,11 @@ use crate::{
|
|||||||
util::{run_cmd, run_cmd_capture, which_tool},
|
util::{run_cmd, run_cmd_capture, which_tool},
|
||||||
Error, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
const ROOT_BASE: &str = "/var/mounts";
|
const ROOT_BASE: &str = "/var/mounts";
|
||||||
const TARGET_SYSTEM: &str = "/var/cache/system";
|
const TARGET_SYSTEM: &str = "/var/cache/system";
|
||||||
@@ -52,6 +54,71 @@ const TARGET_MODULES: &str = "/var/cache/modules";
|
|||||||
const TARGET_VM_META: &str = "/var/cache/vm-meta";
|
const TARGET_VM_META: &str = "/var/cache/vm-meta";
|
||||||
const SUBVOLS: &[&str] = &["system", "etc", "modules", "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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlannedMount {
|
pub struct PlannedMount {
|
||||||
pub uuid: String, // UUID string without prefix
|
pub uuid: String, // UUID string without prefix
|
||||||
@@ -189,11 +256,37 @@ pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
|||||||
.map_err(|e| Error::Mount(format!("failed to create dir {}: {}", sm.target, e)))?;
|
.map_err(|e| Error::Mount(format!("failed to create dir {}: {}", sm.target, e)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut results: Vec<MountResult> = Vec::new();
|
let mut results_map: HashMap<String, MountResult> = HashMap::new();
|
||||||
|
let mut existing_mounts = current_mounts();
|
||||||
|
|
||||||
// Root mounts
|
// Root mounts
|
||||||
for pm in &plan.root_mounts {
|
for pm in &plan.root_mounts {
|
||||||
let source = format!("UUID={}", pm.uuid);
|
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 = [
|
let args = [
|
||||||
mount_tool.as_str(),
|
mount_tool.as_str(),
|
||||||
"-t",
|
"-t",
|
||||||
@@ -204,12 +297,23 @@ pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
|||||||
pm.target.as_str(),
|
pm.target.as_str(),
|
||||||
];
|
];
|
||||||
run_cmd(&args)?;
|
run_cmd(&args)?;
|
||||||
results.push(MountResult {
|
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,
|
source,
|
||||||
target: pm.target.clone(),
|
target: pm.target.clone(),
|
||||||
fstype: pm.fstype.clone(),
|
fstype: pm.fstype.clone(),
|
||||||
options: pm.options.clone(),
|
options: pm.options.clone(),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subvolume creation (create-if-missing) and mounts for the primary
|
// Subvolume creation (create-if-missing) and mounts for the primary
|
||||||
@@ -279,6 +383,31 @@ pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
|||||||
// Subvol mounts
|
// Subvol mounts
|
||||||
for sm in &plan.subvol_mounts {
|
for sm in &plan.subvol_mounts {
|
||||||
let source = format!("UUID={}", sm.uuid);
|
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 = [
|
let args = [
|
||||||
mount_tool.as_str(),
|
mount_tool.as_str(),
|
||||||
"-t",
|
"-t",
|
||||||
@@ -289,14 +418,28 @@ pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
|
|||||||
sm.target.as_str(),
|
sm.target.as_str(),
|
||||||
];
|
];
|
||||||
run_cmd(&args)?;
|
run_cmd(&args)?;
|
||||||
results.push(MountResult {
|
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,
|
source,
|
||||||
target: sm.target.clone(),
|
target: sm.target.clone(),
|
||||||
fstype: sm.fstype.clone(),
|
fstype: sm.fstype.clone(),
|
||||||
options: sm.options.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)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,19 +449,24 @@ pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter only the four subvol targets
|
// 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 wanted = [TARGET_ETC, TARGET_MODULES, TARGET_SYSTEM, TARGET_VM_META];
|
||||||
let mut entries: Vec<&MountResult> = mounts
|
let mut subvol_entries: Vec<&MountResult> = mounts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| wanted.contains(&m.target.as_str()))
|
.filter(|m| wanted.contains(&m.target.as_str()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sort by target path ascending to be deterministic
|
// Sort by target path ascending to be deterministic (roots before subvols).
|
||||||
entries.sort_by(|a, b| a.target.cmp(&b.target));
|
root_entries.sort_by(|a, b| a.target.cmp(&b.target));
|
||||||
|
subvol_entries.sort_by(|a, b| a.target.cmp(&b.target));
|
||||||
|
|
||||||
// Compose lines
|
// Compose lines: include all root mounts first, followed by the four subvol targets.
|
||||||
let mut lines: Vec<String> = Vec::new();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
for m in entries {
|
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",
|
"{} {} {} {} 0 0",
|
||||||
|
|||||||
Reference in New Issue
Block a user