From a86933b12b95055b733f79aefffcf7b719dcc2f5 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 19 Sep 2025 14:59:52 +0200 Subject: [PATCH 01/64] prepare rootfs for loading zfs kernel module --- anylinuxfs/src/main.rs | 2 +- download-dependencies.sh | 1 + init-rootfs/default-alpine-packages.txt | 1 + init-rootfs/main.go | 81 +++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 7d3166e..fcfd091 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1308,7 +1308,7 @@ fn wait_for_file(file: impl AsRef) -> anyhow::Result<()> { Ok(()) } -const ROOTFS_CURRENT_VERSION: &str = "1.1.2"; +const ROOTFS_CURRENT_VERSION: &str = "1.2.0"; fn rootfs_version_matches(config: &Config) -> bool { let root_ver_file_path = config.root_ver_file_path.as_path(); diff --git a/download-dependencies.sh b/download-dependencies.sh index 60d0fba..4a9248f 100755 --- a/download-dependencies.sh +++ b/download-dependencies.sh @@ -5,6 +5,7 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) IMAGE_ARCHIVE_NAME="linux-aarch64-Image-v6.12.34-anylinuxfs.tar.gz" +# TODO: update IMAGE_ARCHIVE_URL="https://github.com/nohajc/libkrunfw/releases/download/v6.12.34-rev3/${IMAGE_ARCHIVE_NAME}" GVPROXY_VERSION="0.8.7" diff --git a/init-rootfs/default-alpine-packages.txt b/init-rootfs/default-alpine-packages.txt index 76e4250..e300604 100644 --- a/init-rootfs/default-alpine-packages.txt +++ b/init-rootfs/default-alpine-packages.txt @@ -9,3 +9,4 @@ mount nfs-utils ntfs-3g ntfs-3g-progs +zfs diff --git a/init-rootfs/main.go b/init-rootfs/main.go index e344f76..a619375 100644 --- a/init-rootfs/main.go +++ b/init-rootfs/main.go @@ -1,6 +1,8 @@ package main import ( + "archive/tar" + "compress/gzip" "context" _ "embed" "flag" @@ -266,6 +268,8 @@ func writeSetupScript(cfg *Config) error { vmSetupScriptContent := fmt.Sprintf(`#!/bin/sh apk --update --no-cache add %s +mv /lib/modules/unknown /lib/modules/$(uname -r) +depmod -a rm -v /etc/idmapd.conf /etc/exports `, packagesStr) @@ -330,6 +334,79 @@ func copyVmproxyBinary(prefixDir, rootfsPath string) error { return nil } +func unpackLinuxModules(prefixDir, rootfsPath string) error { + modulesSrcPath := filepath.Join(prefixDir, "lib", "modules.tar.gz") + modulesDstPath := filepath.Join(rootfsPath, "lib", "modules", "unknown") + err := os.MkdirAll(modulesDstPath, 0755) + if err != nil { + fmt.Printf("Error creating modules directory: %v\n", err) + return err + } + + modulesFile, err := os.Open(modulesSrcPath) + if err != nil { + fmt.Printf("Error opening modules archive: %v\n", err) + return err + } + defer modulesFile.Close() + + gzReader, err := gzip.NewReader(modulesFile) + if err != nil { + fmt.Printf("Error creating gzip reader: %v\n", err) + return err + } + defer gzReader.Close() + + tarReader := tar.NewReader(gzReader) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + fmt.Printf("Error reading tar archive: %v\n", err) + return err + } + + targetPath := filepath.Join(modulesDstPath, header.Name) + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + fmt.Printf("Error creating directory %s: %v\n", targetPath, err) + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + fmt.Printf("Error creating parent directory for %s: %v\n", targetPath, err) + return err + } + outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + fmt.Printf("Error creating file %s: %v\n", targetPath, err) + return err + } + if _, err := io.Copy(outFile, tarReader); err != nil { + outFile.Close() + fmt.Printf("Error writing file %s: %v\n", targetPath, err) + return err + } + outFile.Close() + case tar.TypeSymlink: + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + fmt.Printf("Error creating parent directory for symlink %s: %v\n", targetPath, err) + return err + } + if err := os.Symlink(header.Linkname, targetPath); err != nil { + fmt.Printf("Error creating symlink %s -> %s: %v\n", targetPath, header.Linkname, err) + return err + } + } + } + + return nil +} + func initRootfs(cfg *Config, nameserver string) error { if _, err := os.Stat(cfg.ImageBasePath); err == nil { err = os.RemoveAll(cfg.ImageBasePath) @@ -363,6 +440,10 @@ func initRootfs(cfg *Config, nameserver string) error { return err } + if err := unpackLinuxModules(cfg.PrefixDir, cfg.RootfsPath); err != nil { + return err + } + return copyVmproxyBinary(cfg.PrefixDir, cfg.RootfsPath) } From 9d82a5b32ec4fdbe897c3efbe8981ab66a6d2f36 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 19 Sep 2025 16:37:38 +0200 Subject: [PATCH 02/64] update download-dedpendencies.sh --- download-dependencies.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/download-dependencies.sh b/download-dependencies.sh index 4a9248f..aa47107 100755 --- a/download-dependencies.sh +++ b/download-dependencies.sh @@ -5,8 +5,9 @@ set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) IMAGE_ARCHIVE_NAME="linux-aarch64-Image-v6.12.34-anylinuxfs.tar.gz" -# TODO: update -IMAGE_ARCHIVE_URL="https://github.com/nohajc/libkrunfw/releases/download/v6.12.34-rev3/${IMAGE_ARCHIVE_NAME}" +RELEASE_URL="https://github.com/nohajc/libkrunfw/releases/download/v6.12.34-rev4" +IMAGE_ARCHIVE_URL="${RELEASE_URL}/${IMAGE_ARCHIVE_NAME}" +MODULES_ARCHIVE_URL="${RELEASE_URL}/modules.tar.gz" GVPROXY_VERSION="0.8.7" GVPROXY_URL="https://github.com/containers/gvisor-tap-vsock/releases/download/v${GVPROXY_VERSION}/gvproxy-darwin" @@ -17,5 +18,9 @@ mkdir -p "libexec" tar xzf "$IMAGE_ARCHIVE_NAME" -C "libexec" rm "$IMAGE_ARCHIVE_NAME" +curl -LO "$MODULES_ARCHIVE_URL" +mkdir -p "lib" +mv modules.tar.gz lib/ + curl -L -o libexec/gvproxy "$GVPROXY_URL" chmod +x libexec/gvproxy From 620861317eda99bff8a40b9f3e87f985dab9b52a Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 09:14:36 +0200 Subject: [PATCH 03/64] vmproxy: zpool import/export --- vmproxy/src/main.rs | 97 ++++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 6d6e210..26ed5eb 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -220,6 +220,22 @@ fn statfs(path: impl AsRef) -> io::Result { Ok(buf) } +fn script(script: &str) -> Command { + let mut cmd = Command::new("/bin/busybox"); + cmd.arg("sh").arg("-c").arg(script); + cmd +} + +fn script_output(code: &str) -> anyhow::Result { + Ok(String::from_utf8_lossy( + &script(code) + .output() + .context("Failed to run script command")? + .stdout, + ) + .into()) +} + fn main() -> ExitCode { if let Err(e) = run() { eprintln!("Error: {:#}", e); @@ -244,12 +260,7 @@ fn run() -> anyhow::Result<()> { deferred.add(|| { let kernel_log_warning = "Warning: failed to dump dmesg output to /kernel.log"; - match Command::new("/bin/busybox") - .arg("sh") - .arg("-c") - .arg("dmesg > /kernel.log") - .status() - { + match script("dmesg > /kernel.log").status() { Ok(status) if !status.success() => { eprintln!("{}", kernel_log_warning); } @@ -342,14 +353,11 @@ fn run() -> anyhow::Result<()> { .status() .context("Failed to run mdadm command")?; - let output = Command::new("/bin/busybox") - .arg("sh") - .arg("-c") - .arg("mdadm --detail --scan | cut -d' ' -f2") - .output() - .context("Failed to get RAID device path from mdadm")?; + let md_path = script_output("mdadm --detail --scan | cut -d' ' -f2") + .context("Failed to get RAID device path from mdadm")? + .trim() + .to_owned(); - let md_path = String::from_utf8_lossy(&output.stdout).trim().to_owned(); if !md_path.is_empty() { disk_path = md_path; } @@ -374,10 +382,30 @@ fn run() -> anyhow::Result<()> { _ => {} } let is_logical = disk_path.starts_with("/dev/mapper") || is_raid; + let is_zfs = fs_type.as_deref() == Some("zfs_member"); let name = &cli.mount_name; let mount_name = if !is_logical { - name.to_owned() + if is_zfs { + let label = { + script("modprobe zfs") + .status() + .context("Failed to load zfs module")?; + let zpool_count = script_output("zpool import | grep 'pool:' | wc -l") + .context("Failed to get ZFS member count")? + .parse::()?; + if zpool_count > 1 { + "zpool_root".to_owned() + } else { + script_output("zpool import | grep 'pool:' | head -n1 | awk '{{ print $2 }}'") + .context("Failed to get ZFS pool name")? + } + }; + println!("", &label); + label + } else { + name.to_owned() + } } else { let label = Command::new("/sbin/blkid") .arg(&disk_path) @@ -474,10 +502,16 @@ fn run() -> anyhow::Result<()> { let force_output_off = deferred.add(|| { println!(""); }); - let mnt_result = Command::new("/bin/mount") - .args(mnt_args) - .status() - .context("Failed to run mount command")?; + let mnt_result = if is_zfs { + script(&format!("zpool import -faR {}", &mount_point)) + .status() + .context("Failed to run zpool import command")? + } else { + Command::new("/bin/mount") + .args(mnt_args) + .status() + .context("Failed to run mount command")? + }; if !mnt_result.success() { return Err(anyhow!( @@ -502,8 +536,13 @@ fn run() -> anyhow::Result<()> { deferred.add({ let mount_point = mount_point.clone(); move || { - let mut backoff = Duration::from_secs(1); - while let Err(e) = unmount(&mount_point, UnmountFlags::empty()) { + let mut backoff = Duration::from_millis(50); + let umount_action: &dyn Fn() -> _ = if is_zfs { + &|| script("zpool export -a").status().map(|_| ()) + } else { + &|| unmount(&mount_point, UnmountFlags::empty()) + }; + while let Err(e) = umount_action() { eprintln!("Failed to unmount '{}': {}", &mount_point, e); std::thread::sleep(backoff); backoff = std::cmp::min(backoff * 2, Duration::from_secs(32)); @@ -523,17 +562,13 @@ fn run() -> anyhow::Result<()> { }); let effective_mount_options = { - let output = Command::new("/bin/busybox") - .arg("sh") - .arg("-c") - .arg(format!( - "mount | grep {} | awk -F'(' '{{ print $2 }}' | tr -d ')'", - &disk_path - )) - .output() - .context(format!("Failed to get mount options for {}", &disk_path))?; - - let opts = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let opts = script_output(&format!( + "mount | grep {} | awk -F'(' '{{ print $2 }}' | tr -d ')'", + &disk_path + )) + .with_context(|| format!("Failed to get mount options for {}", &disk_path))? + .trim() + .to_owned(); println!("Effective mount options: {}", opts); opts } From 2a432c7cedb8f58688dda8505255acd81bb3967c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 09:29:19 +0200 Subject: [PATCH 04/64] fix zpool count parsing --- vmproxy/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 26ed5eb..de260a2 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -392,8 +392,10 @@ fn run() -> anyhow::Result<()> { .status() .context("Failed to load zfs module")?; let zpool_count = script_output("zpool import | grep 'pool:' | wc -l") - .context("Failed to get ZFS member count")? - .parse::()?; + .context("Failed to get zpool count")? + .trim() + .parse::() + .context("Failed to parse zpool count")?; if zpool_count > 1 { "zpool_root".to_owned() } else { From 3039a7d4683b99761f5f1e28253d457b4610c97a Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 09:32:43 +0200 Subject: [PATCH 05/64] need to trim zpool name too --- vmproxy/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index de260a2..13fbe16 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -401,6 +401,8 @@ fn run() -> anyhow::Result<()> { } else { script_output("zpool import | grep 'pool:' | head -n1 | awk '{{ print $2 }}'") .context("Failed to get ZFS pool name")? + .trim() + .to_owned() } }; println!("", &label); From ecc142139b98d99a90eb7b199f8e07e72379ad68 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 13:14:46 +0200 Subject: [PATCH 06/64] need to export all zfs datasets --- anylinuxfs/src/main.rs | 2 +- vmproxy/Cargo.lock | 1 + vmproxy/Cargo.toml | 8 ++- vmproxy/src/kernel_cfg.rs | 21 ++++++++ vmproxy/src/main.rs | 55 ++++++++++++++++++--- vmproxy/src/zfs.rs | 100 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 vmproxy/src/kernel_cfg.rs create mode 100644 vmproxy/src/zfs.rs diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index fcfd091..fa0da25 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1204,7 +1204,7 @@ fn wait_for_nfs_server( fn mount_nfs(share_path: &str, config: &MountConfig) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { let mut shell_script = format!( - "mount -t nfs \"localhost:{}\" \"{}\"", + "mount -t nfs -o vers=4 \"localhost:{}\" \"{}\"", share_path, mount_point.display() ); diff --git a/vmproxy/Cargo.lock b/vmproxy/Cargo.lock index da7f934..b1c75a0 100644 --- a/vmproxy/Cargo.lock +++ b/vmproxy/Cargo.lock @@ -1172,6 +1172,7 @@ dependencies = [ "reqwest", "rpassword", "serde", + "serde_json", "sys-mount", "vsock", ] diff --git a/vmproxy/Cargo.toml b/vmproxy/Cargo.toml index d78c0ac..5948d40 100644 --- a/vmproxy/Cargo.toml +++ b/vmproxy/Cargo.toml @@ -6,14 +6,18 @@ edition = "2024" [dependencies] anyhow = "1.0.97" libc = "0.2.171" -procfs = { version = "0.17.0", features = ["flate2"] } + reqwest = { version = "0.12.15", default-features = false, features = [ "blocking", "json", ] } serde = { version = "1.0.219", features = ["derive"] } -sys-mount = { version = "3.0.1", default-features = false } +serde_json = "1.0" vsock = "0.5.1" common_utils = { path = "../common-utils" } clap = { version = "4.5.35", features = ["derive"] } rpassword = "7.4.0" + +[target.'cfg(target_os = "linux")'.dependencies] +procfs = { version = "0.17.0", features = ["flate2"] } +sys-mount = { version = "3.0.1", default-features = false } diff --git a/vmproxy/src/kernel_cfg.rs b/vmproxy/src/kernel_cfg.rs new file mode 100644 index 0000000..c6ba0f4 --- /dev/null +++ b/vmproxy/src/kernel_cfg.rs @@ -0,0 +1,21 @@ +#![allow(unused)] +use anyhow::Context; +use std::collections::HashMap; + +#[cfg(target_os = "linux")] +pub fn kernel_config() -> anyhow::Result> { + // Use procfs only on Linux + let mut out: HashMap = HashMap::new(); + let cfg = procfs::kernel_config().context("failed to read kernel config via procfs")?; + for (k, v) in cfg { + // Convert the procfs ConfigSetting (or similar) to a String representation. + out.insert(k, format!("{:?}", v)); + } + Ok(out) +} + +#[cfg(not(target_os = "linux"))] +pub fn kernel_config() -> anyhow::Result> { + // On non-Linux hosts, return an empty map instead of compiling procfs. + Ok(HashMap::new()) +} diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 13fbe16..1663693 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -11,9 +11,13 @@ use std::path::Path; use std::process::{Child, Command, ExitCode, Stdio}; use std::time::Duration; use std::{fs, io::BufReader}; +#[cfg(target_os = "linux")] use sys_mount::{UnmountFlags, unmount}; use vsock::{VsockAddr, VsockListener}; +mod kernel_cfg; +mod zfs; + #[derive(Parser)] #[command(version, about, long_about = None)] #[clap(disable_help_flag = true)] @@ -236,6 +240,15 @@ fn script_output(code: &str) -> anyhow::Result { .into()) } +fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result { + let mut export_args = format!("{export_mode},no_subtree_check,no_root_squash,insecure"); + if statfs(path)?.f_type == 0x65735546 { + // exporting FUSE requires fsid + export_args += ",fsid=728" + } + Ok(export_args) +} + fn main() -> ExitCode { if let Err(e) = run() { eprintln!("Error: {:#}", e); @@ -397,7 +410,7 @@ fn run() -> anyhow::Result<()> { .parse::() .context("Failed to parse zpool count")?; if zpool_count > 1 { - "zpool_root".to_owned() + "zfs_root".to_owned() } else { script_output("zpool import | grep 'pool:' | head -n1 | awk '{{ print $2 }}'") .context("Failed to get ZFS pool name")? @@ -544,7 +557,14 @@ fn run() -> anyhow::Result<()> { let umount_action: &dyn Fn() -> _ = if is_zfs { &|| script("zpool export -a").status().map(|_| ()) } else { - &|| unmount(&mount_point, UnmountFlags::empty()) + #[cfg(target_os = "linux")] + { + &|| unmount(&mount_point, UnmountFlags::empty()) + } + #[cfg(not(target_os = "linux"))] + { + &|| Ok(()) + } }; while let Err(e) = umount_action() { eprintln!("Failed to unmount '{}': {}", &mount_point, e); @@ -600,12 +620,33 @@ fn run() -> anyhow::Result<()> { }; let export_mode = if effective_read_only { "ro" } else { "rw" }; - let mut export_args = format!("{export_mode},no_subtree_check,no_root_squash,insecure"); - if statfs(&export_path)?.f_type == 0x65735546 { - // exporting FUSE requires fsid - export_args += ",fsid=728" + + let all_exports = if is_zfs { + let mut paths = zfs::mountpoints_from_json( + &script_output("zfs list -j").context("Failed to get ZFS mountpoints")?, + ) + .context("Failed to parse ZFS mountpoints")?; + + if !paths.iter().any(|p| p == &export_path) { + paths.push(export_path); + } + + let mut exports = vec![]; + for p in paths { + let a = export_args_for_path(&p, export_mode)?; + exports.push((p, a)); + } + exports + } else { + // single export + let export_args = export_args_for_path(&export_path, export_mode)?; + vec![(export_path, export_args)] + }; + let mut exports_content = String::new(); + + for (export_path, export_args) in &all_exports { + exports_content += &format!("\"{}\" *({})\n", &export_path, export_args); } - let exports_content = format!("\"{}\" *({})\n", &export_path, export_args); fs::write("/etc/exports", exports_content).context("Failed to write to /etc/exports")?; println!("Successfully initialized /etc/exports."); diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs new file mode 100644 index 0000000..55edf1b --- /dev/null +++ b/vmproxy/src/zfs.rs @@ -0,0 +1,100 @@ +#![allow(unused)] +use anyhow::Context; +use serde::Deserialize; +use std::collections::HashMap; + +/// Top-level structure matching the `zfs list -j` JSON output +#[derive(Debug, Deserialize)] +pub struct ZfsList { + #[serde(rename = "output_version")] + pub output_version: Option, + pub datasets: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct OutputVersion { + pub command: String, + pub vers_major: u32, + pub vers_minor: u32, +} + +#[derive(Debug, Deserialize)] +pub struct Dataset { + pub name: String, + #[serde(rename = "type")] + pub ds_type: String, + pub pool: Option, + pub createtxg: Option, + pub properties: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct Property { + pub value: String, + pub source: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Source { + #[serde(rename = "type")] + pub src_type: Option, + pub data: Option, +} + +/// Parse JSON text containing the ZFS output and return the mountpoint values for every +/// dataset that defines a `mountpoint` property. +pub fn mountpoints_from_json(text: &str) -> anyhow::Result> { + let parsed: ZfsList = serde_json::from_str(text).context("failed to parse zfs json")?; + + let mut out = Vec::new(); + for (_key, ds) in parsed.datasets.into_iter() { + if let Some(props) = ds.properties { + if let Some(mount_prop) = props.get("mountpoint") + && mount_prop.value != "none" + { + out.push(mount_prop.value.clone()); + } + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mountpoints_from_minimal_json() { + let json = r#" + { + "output_version": { "command": "zfs list", "vers_major": 0, "vers_minor": 1 }, + "datasets": { + "pool/ds1": { + "name": "pool/ds1", + "type": "FILESYSTEM", + "pool": "pool", + "createtxg": "1", + "properties": { + "mountpoint": { "value": "/mnt/foo", "source": { "type": "LOCAL", "data": "-" } }, + "used": { "value": "1K", "source": { "type": "NONE", "data": "-" } } + } + }, + "pool/ds2": { + "name": "pool/ds2", + "type": "FILESYSTEM", + "pool": "pool", + "createtxg": "2", + "properties": { + "mountpoint": { "value": "none", "source": { "type": "LOCAL", "data": "-" } } + } + } + } + } + "#; + + let mps = mountpoints_from_json(json).expect("parse should succeed"); + assert_eq!(mps.len(), 2); + assert!(mps.contains(&"/mnt/foo".to_string())); + assert!(mps.contains(&"none".to_string())); + } +} From c8070d5cb84332aa2b965e089c8e8c7e85799777 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 13:17:28 +0200 Subject: [PATCH 07/64] need to debug this --- vmproxy/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 1663693..4e55603 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -627,6 +627,8 @@ fn run() -> anyhow::Result<()> { ) .context("Failed to parse ZFS mountpoints")?; + println!("ZFS mountpoints: {:?}", &paths); + if !paths.iter().any(|p| p == &export_path) { paths.push(export_path); } From 76522a2b99d2d26bfd13968a24fb6f6b603330b3 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 13:21:04 +0200 Subject: [PATCH 08/64] deduplicate zfs dataset mount points --- vmproxy/src/zfs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index 55edf1b..4dc95de 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -1,7 +1,7 @@ #![allow(unused)] use anyhow::Context; use serde::Deserialize; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; /// Top-level structure matching the `zfs list -j` JSON output #[derive(Debug, Deserialize)] @@ -46,17 +46,17 @@ pub struct Source { pub fn mountpoints_from_json(text: &str) -> anyhow::Result> { let parsed: ZfsList = serde_json::from_str(text).context("failed to parse zfs json")?; - let mut out = Vec::new(); + let mut out = HashSet::new(); for (_key, ds) in parsed.datasets.into_iter() { if let Some(props) = ds.properties { if let Some(mount_prop) = props.get("mountpoint") && mount_prop.value != "none" { - out.push(mount_prop.value.clone()); + out.insert(mount_prop.value.clone()); } } } - Ok(out) + Ok(out.into_iter().collect()) } #[cfg(test)] From 5f8c4358629179f335d8be04a4f2b5b6423ea50b Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 15:05:07 +0200 Subject: [PATCH 09/64] we need to parse `zpool import` output after all --- vmproxy/src/main.rs | 30 +++++++------------------ vmproxy/src/utils.rs | 19 ++++++++++++++++ vmproxy/src/zfs.rs | 53 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 vmproxy/src/utils.rs diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 4e55603..569a8e0 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -15,7 +15,10 @@ use std::{fs, io::BufReader}; use sys_mount::{UnmountFlags, unmount}; use vsock::{VsockAddr, VsockListener}; +use crate::utils::{script, script_output}; + mod kernel_cfg; +mod utils; mod zfs; #[derive(Parser)] @@ -224,22 +227,6 @@ fn statfs(path: impl AsRef) -> io::Result { Ok(buf) } -fn script(script: &str) -> Command { - let mut cmd = Command::new("/bin/busybox"); - cmd.arg("sh").arg("-c").arg(script); - cmd -} - -fn script_output(code: &str) -> anyhow::Result { - Ok(String::from_utf8_lossy( - &script(code) - .output() - .context("Failed to run script command")? - .stdout, - ) - .into()) -} - fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result { let mut export_args = format!("{export_mode},no_subtree_check,no_root_squash,insecure"); if statfs(path)?.f_type == 0x65735546 { @@ -404,6 +391,10 @@ fn run() -> anyhow::Result<()> { script("modprobe zfs") .status() .context("Failed to load zfs module")?; + + let zpools = zfs::get_importable_zpools()?; + println!("Importable zpools: {:?}", &zpools); + let zpool_count = script_output("zpool import | grep 'pool:' | wc -l") .context("Failed to get zpool count")? .trim() @@ -622,12 +613,7 @@ fn run() -> anyhow::Result<()> { let export_mode = if effective_read_only { "ro" } else { "rw" }; let all_exports = if is_zfs { - let mut paths = zfs::mountpoints_from_json( - &script_output("zfs list -j").context("Failed to get ZFS mountpoints")?, - ) - .context("Failed to parse ZFS mountpoints")?; - - println!("ZFS mountpoints: {:?}", &paths); + let mut paths = zfs::mountpoints()?; if !paths.iter().any(|p| p == &export_path) { paths.push(export_path); diff --git a/vmproxy/src/utils.rs b/vmproxy/src/utils.rs new file mode 100644 index 0000000..59be48e --- /dev/null +++ b/vmproxy/src/utils.rs @@ -0,0 +1,19 @@ +use std::process::Command; + +use anyhow::Context; + +pub fn script(script: &str) -> Command { + let mut cmd = Command::new("/bin/busybox"); + cmd.arg("sh").arg("-c").arg(script); + cmd +} + +pub fn script_output(code: &str) -> anyhow::Result { + Ok(String::from_utf8_lossy( + &script(code) + .output() + .context("Failed to run script command")? + .stdout, + ) + .into()) +} diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index 4dc95de..ada8ad8 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -3,6 +3,50 @@ use anyhow::Context; use serde::Deserialize; use std::collections::{HashMap, HashSet}; +use crate::utils::script_output; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Zpool { + name: String, + id: String, +} + +pub fn get_importable_zpools() -> anyhow::Result> { + // pool: rpool + // id: 12902241841912726807 + // pool: bpool + // id: 16435365342370519676 + let text = + script_output("zpool import | grep -E '(pool|id):'").context("Failed to get zpools")?; + + let mut zpool_names = HashMap::new(); + let mut zpools = Vec::new(); + for line in text.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("pool: ") { + let mut name = trimmed.strip_prefix("pool: ").unwrap().to_string(); + + let cnt = zpool_names.entry(name.clone()).or_insert(0); + if *cnt > 0 { + name = format!("{}-{}", name, cnt); + } + *cnt += 1; + + zpools.push(Zpool { + name, + id: String::new(), + }); + } else if trimmed.starts_with("id: ") { + let id = trimmed.strip_prefix("id: ").unwrap().to_string(); + if let Some(last) = zpools.iter_mut().last() { + last.id = id; + } + } + } + + Ok(zpools) +} + /// Top-level structure matching the `zfs list -j` JSON output #[derive(Debug, Deserialize)] pub struct ZfsList { @@ -43,8 +87,13 @@ pub struct Source { /// Parse JSON text containing the ZFS output and return the mountpoint values for every /// dataset that defines a `mountpoint` property. -pub fn mountpoints_from_json(text: &str) -> anyhow::Result> { - let parsed: ZfsList = serde_json::from_str(text).context("failed to parse zfs json")?; +pub fn mountpoints() -> anyhow::Result> { + let text = script_output("zfs list -j").context("Failed to get ZFS mountpoints")?; + mountpoints_from_json(&text) +} + +fn mountpoints_from_json(text: &str) -> anyhow::Result> { + let parsed: ZfsList = serde_json::from_str(&text).context("failed to parse zfs json")?; let mut out = HashSet::new(); for (_key, ds) in parsed.datasets.into_iter() { From 44b615bbb938f7c084761ebac395aeb71c9b2b5e Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 16:45:35 +0200 Subject: [PATCH 10/64] determine correct order before mounting zpool datasets --- vmproxy/src/main.rs | 30 ++++++++------------ vmproxy/src/zfs.rs | 69 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 569a8e0..138d4b2 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -393,20 +393,12 @@ fn run() -> anyhow::Result<()> { .context("Failed to load zfs module")?; let zpools = zfs::get_importable_zpools()?; - println!("Importable zpools: {:?}", &zpools); - - let zpool_count = script_output("zpool import | grep 'pool:' | wc -l") - .context("Failed to get zpool count")? - .trim() - .parse::() - .context("Failed to parse zpool count")?; - if zpool_count > 1 { + // println!("Importable zpools: {:?}", &zpools); + + if zpools.len() > 1 { "zfs_root".to_owned() } else { - script_output("zpool import | grep 'pool:' | head -n1 | awk '{{ print $2 }}'") - .context("Failed to get ZFS pool name")? - .trim() - .to_owned() + zpools[0].name.clone() } }; println!("", &label); @@ -510,15 +502,15 @@ fn run() -> anyhow::Result<()> { let force_output_off = deferred.add(|| { println!(""); }); - let mnt_result = if is_zfs { - script(&format!("zpool import -faR {}", &mount_point)) - .status() - .context("Failed to run zpool import command")? + + let (mnt_result, zfs_mountpoints) = if is_zfs { + zfs::import_all_zpools_and_mount_in_correct_order(&mount_point)? } else { - Command::new("/bin/mount") + let res = Command::new("/bin/mount") .args(mnt_args) .status() - .context("Failed to run mount command")? + .context("Failed to run mount command")?; + (res, vec![]) }; if !mnt_result.success() { @@ -613,7 +605,7 @@ fn run() -> anyhow::Result<()> { let export_mode = if effective_read_only { "ro" } else { "rw" }; let all_exports = if is_zfs { - let mut paths = zfs::mountpoints()?; + let mut paths: Vec<_> = zfs_mountpoints.into_iter().map(|m| m.path).collect(); if !paths.iter().any(|p| p == &export_path) { paths.push(export_path); diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index ada8ad8..daa42ef 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -1,14 +1,17 @@ #![allow(unused)] use anyhow::Context; use serde::Deserialize; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + process::ExitStatus, +}; -use crate::utils::script_output; +use crate::utils::{script, script_output}; #[derive(Debug, PartialEq, Eq, Hash)] pub struct Zpool { - name: String, - id: String, + pub name: String, + pub id: String, } pub fn get_importable_zpools() -> anyhow::Result> { @@ -67,7 +70,7 @@ pub struct Dataset { pub name: String, #[serde(rename = "type")] pub ds_type: String, - pub pool: Option, + pub pool: String, pub createtxg: Option, pub properties: Option>, } @@ -85,14 +88,20 @@ pub struct Source { pub data: Option, } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Mountpoint { + pub path: String, + pub pool: String, +} + /// Parse JSON text containing the ZFS output and return the mountpoint values for every /// dataset that defines a `mountpoint` property. -pub fn mountpoints() -> anyhow::Result> { +pub fn mountpoints() -> anyhow::Result> { let text = script_output("zfs list -j").context("Failed to get ZFS mountpoints")?; mountpoints_from_json(&text) } -fn mountpoints_from_json(text: &str) -> anyhow::Result> { +fn mountpoints_from_json(text: &str) -> anyhow::Result> { let parsed: ZfsList = serde_json::from_str(&text).context("failed to parse zfs json")?; let mut out = HashSet::new(); @@ -101,11 +110,45 @@ fn mountpoints_from_json(text: &str) -> anyhow::Result> { if let Some(mount_prop) = props.get("mountpoint") && mount_prop.value != "none" { - out.insert(mount_prop.value.clone()); + out.insert(Mountpoint { + path: mount_prop.value.clone(), + pool: ds.pool.clone(), + }); } } } - Ok(out.into_iter().collect()) + let mut res: Vec<_> = out.into_iter().collect(); + // sort by path lexicographically + res.sort_by(|a, b| a.path.cmp(&b.path)); + Ok(res) +} + +pub fn import_all_zpools_and_mount_in_correct_order( + mount_point_root: &str, +) -> anyhow::Result<(ExitStatus, Vec)> { + let res = script(&format!("zpool import -faNR {}", &mount_point_root)) + .status() + .context("Failed to run zpool import command")?; + + let zfs_mountpoints = mountpoints().context("Failed to get ZFS mountpoints after import")?; + println!("ZFS mountpoints"); + let mut mounted_zpools = HashSet::new(); + + for mp in &zfs_mountpoints { + println!(" {:?}", mp); + } + + for mp in &zfs_mountpoints { + if mounted_zpools.insert(mp.pool.clone()) { + // first time seeing this pool + println!("Mounting pool {}", &mp.pool); + script(&format!("zfs mount -R {}", mp.pool)) + .status() + .with_context(|| format!("Failed to mount ZFS pool {}", mp.pool))?; + } + } + + Ok((res, zfs_mountpoints)) } #[cfg(test)] @@ -142,8 +185,10 @@ mod tests { "#; let mps = mountpoints_from_json(json).expect("parse should succeed"); - assert_eq!(mps.len(), 2); - assert!(mps.contains(&"/mnt/foo".to_string())); - assert!(mps.contains(&"none".to_string())); + assert_eq!(mps.len(), 1); + assert!(mps.contains(&Mountpoint { + path: "/mnt/foo".into(), + pool: "pool".into(), + })); } } From 05b6eb80e41f25b20a1082594188202009cfd343 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 17:16:22 +0200 Subject: [PATCH 11/64] disable debug prints, rename zfs_member to zfs --- vmproxy/src/main.rs | 4 ++++ vmproxy/src/zfs.rs | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 138d4b2..8929dfe 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -439,6 +439,10 @@ fn run() -> anyhow::Result<()> { println!("", &fs); fs_type = if !fs.is_empty() { Some(fs) } else { None }; } + Some("zfs_member") => { + fs_type = Some("zfs".to_owned()); + println!("", fs_type.as_deref().unwrap()); + } _ => (), } diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index daa42ef..d0e2596 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -131,17 +131,17 @@ pub fn import_all_zpools_and_mount_in_correct_order( .context("Failed to run zpool import command")?; let zfs_mountpoints = mountpoints().context("Failed to get ZFS mountpoints after import")?; - println!("ZFS mountpoints"); + // println!("ZFS mountpoints"); let mut mounted_zpools = HashSet::new(); - for mp in &zfs_mountpoints { - println!(" {:?}", mp); - } + // for mp in &zfs_mountpoints { + // println!(" {:?}", mp); + // } for mp in &zfs_mountpoints { if mounted_zpools.insert(mp.pool.clone()) { // first time seeing this pool - println!("Mounting pool {}", &mp.pool); + // println!("Mounting pool {}", &mp.pool); script(&format!("zfs mount -R {}", mp.pool)) .status() .with_context(|| format!("Failed to mount ZFS pool {}", mp.pool))?; From 361e1e39b84fa8d0e59d1a8d5c9eb72cb45c5072 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 17:57:14 +0200 Subject: [PATCH 12/64] only prepare mnt_args when needed --- vmproxy/src/main.rs | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 8929dfe..eae5074 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -479,26 +479,31 @@ fn run() -> anyhow::Result<()> { // .mount("/dev/vda", &mount_point) // .context(format!("Failed to mount '/dev/vda' on '{}'", &mount_point))?; - let mnt_args = [ - "-t", - fs_driver - .as_deref() - .or(fs_type.as_deref()) - .unwrap_or("auto"), - &disk_path, - &mount_point, - ] - .into_iter() - .chain( - mount_options - .as_deref() - .into_iter() - .flat_map(|opts| ["-o", opts]), - ) - .chain(verbose.then_some("-v").into_iter()); - - let mnt_args: Vec<&str> = mnt_args.collect(); - println!("mount args: {:?}", &mnt_args); + let mnt_args = if !is_zfs { + let mnt_args = [ + "-t", + fs_driver + .as_deref() + .or(fs_type.as_deref()) + .unwrap_or("auto"), + &disk_path, + &mount_point, + ] + .into_iter() + .chain( + mount_options + .as_deref() + .into_iter() + .flat_map(|opts| ["-o", opts]), + ) + .chain(verbose.then_some("-v").into_iter()); + + let mnt_args: Vec<&str> = mnt_args.collect(); + println!("mount args: {:?}", &mnt_args); + mnt_args + } else { + vec![] + }; // we must show any output of mount command // in case there's a warning (e.g. NTFS cannot be accessed rw) From fea5a7fe4f40275fb6cc388b18f1ba9e1c3d05aa Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 18:07:18 +0200 Subject: [PATCH 13/64] zfs import/mount error handling --- vmproxy/src/zfs.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index d0e2596..42d112f 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -6,7 +6,10 @@ use std::{ process::ExitStatus, }; -use crate::utils::{script, script_output}; +use crate::{ + utils::{script, script_output}, + zfs, +}; #[derive(Debug, PartialEq, Eq, Hash)] pub struct Zpool { @@ -94,8 +97,6 @@ pub struct Mountpoint { pub pool: String, } -/// Parse JSON text containing the ZFS output and return the mountpoint values for every -/// dataset that defines a `mountpoint` property. pub fn mountpoints() -> anyhow::Result> { let text = script_output("zfs list -j").context("Failed to get ZFS mountpoints")?; mountpoints_from_json(&text) @@ -130,6 +131,10 @@ pub fn import_all_zpools_and_mount_in_correct_order( .status() .context("Failed to run zpool import command")?; + if !res.success() { + return Ok((res, Vec::new())); + } + let zfs_mountpoints = mountpoints().context("Failed to get ZFS mountpoints after import")?; // println!("ZFS mountpoints"); let mut mounted_zpools = HashSet::new(); @@ -142,9 +147,13 @@ pub fn import_all_zpools_and_mount_in_correct_order( if mounted_zpools.insert(mp.pool.clone()) { // first time seeing this pool // println!("Mounting pool {}", &mp.pool); - script(&format!("zfs mount -R {}", mp.pool)) + let status = script(&format!("zfs mount -R {}", mp.pool)) .status() .with_context(|| format!("Failed to mount ZFS pool {}", mp.pool))?; + + if !status.success() { + return Ok((res, zfs_mountpoints)); + } } } From 38c1837e9279e8677779dc2e63c164e1d04df47f Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 21:28:43 +0200 Subject: [PATCH 14/64] split zpool import and zfs mount operations --- vmproxy/src/main.rs | 25 ++++++++++++++++++++----- vmproxy/src/zfs.rs | 17 +++++++++-------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index eae5074..9835ff1 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -479,6 +479,22 @@ fn run() -> anyhow::Result<()> { // .mount("/dev/vda", &mount_point) // .context(format!("Failed to mount '/dev/vda' on '{}'", &mount_point))?; + let zfs_mountpoints = if is_zfs { + let (status, mountpoints) = zfs::import_all_zpools(&mount_point)?; + if !status.success() { + return Err(anyhow!( + "Importing zpools failed with error code {}", + status + .code() + .map(|c| c.to_string()) + .unwrap_or("unknown".to_owned()) + )); + } + mountpoints + } else { + vec![] + }; + let mnt_args = if !is_zfs { let mnt_args = [ "-t", @@ -512,14 +528,13 @@ fn run() -> anyhow::Result<()> { println!(""); }); - let (mnt_result, zfs_mountpoints) = if is_zfs { - zfs::import_all_zpools_and_mount_in_correct_order(&mount_point)? + let mnt_result = if is_zfs { + zfs::mount_datasets(&zfs_mountpoints)? } else { - let res = Command::new("/bin/mount") + Command::new("/bin/mount") .args(mnt_args) .status() - .context("Failed to run mount command")?; - (res, vec![]) + .context("Failed to run mount command")? }; if !mnt_result.success() { diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index 42d112f..a97a098 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -124,9 +124,7 @@ fn mountpoints_from_json(text: &str) -> anyhow::Result> { Ok(res) } -pub fn import_all_zpools_and_mount_in_correct_order( - mount_point_root: &str, -) -> anyhow::Result<(ExitStatus, Vec)> { +pub fn import_all_zpools(mount_point_root: &str) -> anyhow::Result<(ExitStatus, Vec)> { let res = script(&format!("zpool import -faNR {}", &mount_point_root)) .status() .context("Failed to run zpool import command")?; @@ -136,6 +134,10 @@ pub fn import_all_zpools_and_mount_in_correct_order( } let zfs_mountpoints = mountpoints().context("Failed to get ZFS mountpoints after import")?; + Ok((res, zfs_mountpoints)) +} + +pub fn mount_datasets(mountpoints: &[Mountpoint]) -> anyhow::Result { // println!("ZFS mountpoints"); let mut mounted_zpools = HashSet::new(); @@ -143,21 +145,20 @@ pub fn import_all_zpools_and_mount_in_correct_order( // println!(" {:?}", mp); // } - for mp in &zfs_mountpoints { + for mp in mountpoints { if mounted_zpools.insert(mp.pool.clone()) { // first time seeing this pool // println!("Mounting pool {}", &mp.pool); - let status = script(&format!("zfs mount -R {}", mp.pool)) + let status = script(&format!("zfs mount -lR {}", mp.pool)) .status() .with_context(|| format!("Failed to mount ZFS pool {}", mp.pool))?; if !status.success() { - return Ok((res, zfs_mountpoints)); + return Ok(status); } } } - - Ok((res, zfs_mountpoints)) + Ok(ExitStatus::default()) } #[cfg(test)] From a735325dc54a493dd61b1339103b7fbb6519465d Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 21:37:13 +0200 Subject: [PATCH 15/64] custom actions: added before_mount hook --- common-utils/src/lib.rs | 6 ++++-- vmproxy/src/main.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/common-utils/src/lib.rs b/common-utils/src/lib.rs index 936277b..03b8a4e 100644 --- a/common-utils/src/lib.rs +++ b/common-utils/src/lib.rs @@ -115,6 +115,8 @@ impl<'a> Drop for Deferred<'a> { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CustomActionConfig { + #[serde(default)] + pub before_mount: String, #[serde(default)] pub after_mount: String, #[serde(default)] @@ -128,8 +130,8 @@ pub struct CustomActionConfig { impl CustomActionConfig { pub const VM_EXPORTED_VARS: &[&str] = &["ALFS_VM_MOUNT_POINT"]; - pub fn all_scripts(&self) -> [&str; 2] { - [&self.after_mount, &self.before_unmount] + pub fn all_scripts(&self) -> [&str; 3] { + [&self.before_mount, &self.after_mount, &self.before_unmount] } const PERCENT_ENCODE_SET: &AsciiSet = &CONTROLS.add(b' '); diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 9835ff1..109086d 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -194,6 +194,19 @@ impl CustomActionRunner { Ok(()) } + pub fn before_mount(&self) -> anyhow::Result<()> { + if let Some(action) = &self.config { + if !action.before_mount.is_empty() { + println!(""); + println!("Running before_mount action: `{}`", action.before_mount); + let result = self.execute_action(&action.before_mount); + println!(""); + result?; + } + } + Ok(()) + } + pub fn after_mount(&self) -> anyhow::Result<()> { if let Some(action) = &self.config { if !action.after_mount.is_empty() { @@ -495,6 +508,10 @@ fn run() -> anyhow::Result<()> { vec![] }; + custom_action + .before_mount() + .context("before_mount action")?; + let mnt_args = if !is_zfs { let mnt_args = [ "-t", From de309f7ba3ebdffeea33c705c7d9e66fdfe6e217 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 21:47:37 +0200 Subject: [PATCH 16/64] allow defining environment variables in custom action config --- anylinuxfs/src/main.rs | 4 ++++ common-utils/src/lib.rs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index fa0da25..9f5c0d7 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -167,6 +167,10 @@ impl CustomActionEnvironment for CustomActionConfig { let mut undefined_vars = Vec::new(); let mut env_vars = Vec::new(); + for var_str in &self.environment { + // TODO: format could be validated here + env_vars.push(var_str.clone()); + } for var_name in referenced_variables.iter().chain(&self.capture_environment) { match env::var(&var_name) { Ok(var_value) => { diff --git a/common-utils/src/lib.rs b/common-utils/src/lib.rs index 03b8a4e..a10296e 100644 --- a/common-utils/src/lib.rs +++ b/common-utils/src/lib.rs @@ -122,6 +122,8 @@ pub struct CustomActionConfig { #[serde(default)] pub before_unmount: String, #[serde(default)] + pub environment: Vec, // KEY=value format + #[serde(default)] pub capture_environment: Vec, #[serde(default)] pub override_nfs_export: String, From 8549318fe8a96314a399f514076447f3822a41c4 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 22:06:40 +0200 Subject: [PATCH 17/64] custom actions: fix undefined vars detection --- anylinuxfs/src/main.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 9f5c0d7..78bbf10 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -165,10 +165,16 @@ impl CustomActionEnvironment for CustomActionConfig { referenced_variables.extend(utils::find_env_vars(script)); } + let mut predefined_vars = HashSet::new(); let mut undefined_vars = Vec::new(); let mut env_vars = Vec::new(); for var_str in &self.environment { // TODO: format could be validated here + let var_name = var_str + .split('=') + .next() + .ok_or_else(|| anyhow!("invalid environment variable format: {}", var_str))?; + predefined_vars.insert(var_name.to_owned()); env_vars.push(var_str.clone()); } for var_name in referenced_variables.iter().chain(&self.capture_environment) { @@ -177,7 +183,9 @@ impl CustomActionEnvironment for CustomActionConfig { env_vars.push(format!("{}={}", var_name, var_value)); } Err(VarError::NotPresent) => { - if !Self::VM_EXPORTED_VARS.contains(&var_name.as_str()) { + if !Self::VM_EXPORTED_VARS.contains(&var_name.as_str()) + && !predefined_vars.contains(var_name) + { undefined_vars.push(var_name.clone()); } } From 8e110117da3f5957b9c2bd7ebac56a76e6ae4985 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 22:17:53 +0200 Subject: [PATCH 18/64] better error message when statfs fails --- vmproxy/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 109086d..5d599df 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -242,7 +242,11 @@ fn statfs(path: impl AsRef) -> io::Result { fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result { let mut export_args = format!("{export_mode},no_subtree_check,no_root_squash,insecure"); - if statfs(path)?.f_type == 0x65735546 { + if statfs(path) + .with_context(|| format!("statfs failed for {path}"))? + .f_type + == 0x65735546 + { // exporting FUSE requires fsid export_args += ",fsid=728" } From 4325696d55fea1a50f0988013bb104d4a4ec2401 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 20 Sep 2025 22:23:17 +0200 Subject: [PATCH 19/64] fix zfs mountpoint list filtering --- vmproxy/src/zfs.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index a97a098..97b071c 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -102,6 +102,8 @@ pub fn mountpoints() -> anyhow::Result> { mountpoints_from_json(&text) } +const EXCLUDED_MOUNTPOINT_TYPES: &[&str] = &["-", "legacy", "none"]; + fn mountpoints_from_json(text: &str) -> anyhow::Result> { let parsed: ZfsList = serde_json::from_str(&text).context("failed to parse zfs json")?; @@ -109,7 +111,7 @@ fn mountpoints_from_json(text: &str) -> anyhow::Result> { for (_key, ds) in parsed.datasets.into_iter() { if let Some(props) = ds.properties { if let Some(mount_prop) = props.get("mountpoint") - && mount_prop.value != "none" + && !EXCLUDED_MOUNTPOINT_TYPES.contains(&mount_prop.value.as_str()) { out.insert(Mountpoint { path: mount_prop.value.clone(), From 5cc54e879a1057033e4946d6092ec1a7d1499c1c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sun, 21 Sep 2025 16:44:45 +0200 Subject: [PATCH 20/64] `anylinuxfs init`: install kernel modules from squashfs archive --- download-dependencies.sh | 5 +- init-rootfs/default-alpine-packages.txt | 1 + init-rootfs/main.go | 98 ++++++------------------- vmproxy/src/main.rs | 2 +- 4 files changed, 28 insertions(+), 78 deletions(-) diff --git a/download-dependencies.sh b/download-dependencies.sh index aa47107..5955284 100755 --- a/download-dependencies.sh +++ b/download-dependencies.sh @@ -7,7 +7,8 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) IMAGE_ARCHIVE_NAME="linux-aarch64-Image-v6.12.34-anylinuxfs.tar.gz" RELEASE_URL="https://github.com/nohajc/libkrunfw/releases/download/v6.12.34-rev4" IMAGE_ARCHIVE_URL="${RELEASE_URL}/${IMAGE_ARCHIVE_NAME}" -MODULES_ARCHIVE_URL="${RELEASE_URL}/modules.tar.gz" +MODULES_ARCHIVE_NAME="modules.squashfs" +MODULES_ARCHIVE_URL="${RELEASE_URL}/${MODULES_ARCHIVE_NAME}" GVPROXY_VERSION="0.8.7" GVPROXY_URL="https://github.com/containers/gvisor-tap-vsock/releases/download/v${GVPROXY_VERSION}/gvproxy-darwin" @@ -20,7 +21,7 @@ rm "$IMAGE_ARCHIVE_NAME" curl -LO "$MODULES_ARCHIVE_URL" mkdir -p "lib" -mv modules.tar.gz lib/ +mv ${MODULES_ARCHIVE_NAME} lib/ curl -L -o libexec/gvproxy "$GVPROXY_URL" chmod +x libexec/gvproxy diff --git a/init-rootfs/default-alpine-packages.txt b/init-rootfs/default-alpine-packages.txt index e300604..a5302c0 100644 --- a/init-rootfs/default-alpine-packages.txt +++ b/init-rootfs/default-alpine-packages.txt @@ -9,4 +9,5 @@ mount nfs-utils ntfs-3g ntfs-3g-progs +squashfs-tools zfs diff --git a/init-rootfs/main.go b/init-rootfs/main.go index a619375..b79db95 100644 --- a/init-rootfs/main.go +++ b/init-rootfs/main.go @@ -1,8 +1,6 @@ package main import ( - "archive/tar" - "compress/gzip" "context" _ "embed" "flag" @@ -268,7 +266,11 @@ func writeSetupScript(cfg *Config) error { vmSetupScriptContent := fmt.Sprintf(`#!/bin/sh apk --update --no-cache add %s -mv /lib/modules/unknown /lib/modules/$(uname -r) +MOD_PATH="modules/$(uname -r)" +cd /lib +mkdir -p $MOD_PATH +unsquashfs -d $MOD_PATH modules.squashfs +rm modules.squashfs depmod -a rm -v /etc/idmapd.conf /etc/exports `, packagesStr) @@ -317,15 +319,20 @@ func downloadEntrypointScript(rootfsPath string) error { return nil } -func copyVmproxyBinary(prefixDir, rootfsPath string) error { - vmproxySrcPath := filepath.Join(prefixDir, "libexec", "vmproxy") - vmproxyDstPath := filepath.Join(rootfsPath, "vmproxy") - - copyCmd := exec.Command("cp", "-v", vmproxySrcPath, vmproxyDstPath) +func copyFile(srcPath, dstPath string) error { + copyCmd := exec.Command("cp", "-v", srcPath, dstPath) copyCmd.Stdout = os.Stdout copyCmd.Stderr = os.Stderr - err := copyCmd.Run() + return copyCmd.Run() +} + +func copyVmproxyBinary(prefixDir, rootfsPath string) error { + vmproxyBin := "vmproxy" + vmproxySrcPath := filepath.Join(prefixDir, "libexec", vmproxyBin) + vmproxyDstPath := filepath.Join(rootfsPath, vmproxyBin) + + err := copyFile(vmproxySrcPath, vmproxyDstPath) if err != nil { fmt.Printf("Error copying vmproxy: %v\n", err) return err @@ -334,75 +341,16 @@ func copyVmproxyBinary(prefixDir, rootfsPath string) error { return nil } -func unpackLinuxModules(prefixDir, rootfsPath string) error { - modulesSrcPath := filepath.Join(prefixDir, "lib", "modules.tar.gz") - modulesDstPath := filepath.Join(rootfsPath, "lib", "modules", "unknown") - err := os.MkdirAll(modulesDstPath, 0755) - if err != nil { - fmt.Printf("Error creating modules directory: %v\n", err) - return err - } +func copyLinuxModules(prefixDir, rootfsPath string) error { + modulesSquashfs := "modules.squashfs" + modulesSrcPath := filepath.Join(prefixDir, "lib", modulesSquashfs) + modulesDstPath := filepath.Join(rootfsPath, "lib", modulesSquashfs) - modulesFile, err := os.Open(modulesSrcPath) + err := copyFile(modulesSrcPath, modulesDstPath) if err != nil { - fmt.Printf("Error opening modules archive: %v\n", err) - return err - } - defer modulesFile.Close() - - gzReader, err := gzip.NewReader(modulesFile) - if err != nil { - fmt.Printf("Error creating gzip reader: %v\n", err) + fmt.Printf("Error copying vmproxy: %v\n", err) return err } - defer gzReader.Close() - - tarReader := tar.NewReader(gzReader) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - fmt.Printf("Error reading tar archive: %v\n", err) - return err - } - - targetPath := filepath.Join(modulesDstPath, header.Name) - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { - fmt.Printf("Error creating directory %s: %v\n", targetPath, err) - return err - } - case tar.TypeReg: - if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { - fmt.Printf("Error creating parent directory for %s: %v\n", targetPath, err) - return err - } - outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) - if err != nil { - fmt.Printf("Error creating file %s: %v\n", targetPath, err) - return err - } - if _, err := io.Copy(outFile, tarReader); err != nil { - outFile.Close() - fmt.Printf("Error writing file %s: %v\n", targetPath, err) - return err - } - outFile.Close() - case tar.TypeSymlink: - if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { - fmt.Printf("Error creating parent directory for symlink %s: %v\n", targetPath, err) - return err - } - if err := os.Symlink(header.Linkname, targetPath); err != nil { - fmt.Printf("Error creating symlink %s -> %s: %v\n", targetPath, header.Linkname, err) - return err - } - } - } return nil } @@ -440,7 +388,7 @@ func initRootfs(cfg *Config, nameserver string) error { return err } - if err := unpackLinuxModules(cfg.PrefixDir, cfg.RootfsPath); err != nil { + if err := copyLinuxModules(cfg.PrefixDir, cfg.RootfsPath); err != nil { return err } diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 5d599df..1ebcf84 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -248,7 +248,7 @@ fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result == 0x65735546 { // exporting FUSE requires fsid - export_args += ",fsid=728" + export_args += ",fsid=728" // TODO: unique fsid } Ok(export_args) } From 607af890b803aeb44c263562d43e28488313a4a5 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Mon, 22 Sep 2025 23:09:11 +0200 Subject: [PATCH 21/64] NFSv4 shouldn't be used for everything because of the problems with unmounting --- anylinuxfs/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 78bbf10..5453745 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1216,7 +1216,7 @@ fn wait_for_nfs_server( fn mount_nfs(share_path: &str, config: &MountConfig) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { let mut shell_script = format!( - "mount -t nfs -o vers=4 \"localhost:{}\" \"{}\"", + "mount -t nfs \"localhost:{}\" \"{}\"", share_path, mount_point.display() ); From 367727b9f7f780dfe9e96d28359447fec94b5ef6 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Wed, 24 Sep 2025 21:43:33 +0200 Subject: [PATCH 22/64] use NFSv4 when the filesystem is zfs --- anylinuxfs/src/main.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 5453745..95e8b30 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1213,10 +1213,12 @@ fn wait_for_nfs_server( Ok(nfs_ready) } -fn mount_nfs(share_path: &str, config: &MountConfig) -> anyhow::Result<()> { +fn mount_nfs(share_path: &str, config: &MountConfig, vers4: bool) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { + let opts = if vers4 { "-o vers=4" } else { "" }; let mut shell_script = format!( - "mount -t nfs \"localhost:{}\" \"{}\"", + "mount -t nfs {} \"localhost:{}\" \"{}\"", + opts, share_path, mount_point.display() ); @@ -2245,7 +2247,13 @@ impl AppRunner { } _ => format!("/mnt/{share_name}"), }; - let mount_result = mount_nfs(&share_path, &config); + let vers4 = mnt_dev_info.fs_type() == Some("zfs"); + if vers4 { + let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); + fs::create_dir_all(&mnt_point)?; + config.custom_mount_point = Some(mnt_point); + } + let mount_result = mount_nfs(&share_path, &config, vers4); match &mount_result { Ok(_) => host_println!("Requested NFS share mount"), Err(e) => host_eprintln!("Failed to request NFS mount: {:#}", e), From 9f544977dc6b0f0d8fa72024428bce424e366616 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Wed, 24 Sep 2025 22:17:08 +0200 Subject: [PATCH 23/64] error handling --- anylinuxfs/src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 95e8b30..d8e9edf 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2250,7 +2250,13 @@ impl AppRunner { let vers4 = mnt_dev_info.fs_type() == Some("zfs"); if vers4 { let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); - fs::create_dir_all(&mnt_point)?; + if let Err(e) = fs::create_dir_all(&mnt_point) { + host_eprintln!( + "Failed to create mount point {}: {:#}", + mnt_point.display(), + e + ); + } config.custom_mount_point = Some(mnt_point); } let mount_result = mount_nfs(&share_path, &config, vers4); From f7fe8578f0b8411825060681823c785734e9452e Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Wed, 24 Sep 2025 23:44:32 +0200 Subject: [PATCH 24/64] if we chown the mountpoint to invoker uid/gid, it does exactly what we need --- anylinuxfs/src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index d8e9edf..be038ae 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2256,6 +2256,16 @@ impl AppRunner { mnt_point.display(), e ); + } else if let Err(e) = chown( + &mnt_point, + Some(config.common.invoker_uid), + Some(config.common.invoker_gid), + ) { + host_eprintln!( + "Failed to change owner of {}: {:#}", + mnt_point.display(), + e + ); } config.custom_mount_point = Some(mnt_point); } From 8a0041b3c008f2b2436da87b7fbbe6c388fe109b Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 20:21:30 +0200 Subject: [PATCH 25/64] update LINUX_LABELS --- anylinuxfs/src/diskutil.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/anylinuxfs/src/diskutil.rs b/anylinuxfs/src/diskutil.rs index bc335b6..1e37399 100644 --- a/anylinuxfs/src/diskutil.rs +++ b/anylinuxfs/src/diskutil.rs @@ -156,6 +156,7 @@ pub const LINUX_LABELS: Labels = Labels { "Linux_LVM", "Linux_RAID", "Linux", + "ZFS", ]), fs_types: FsTypes(&[ "btrfs", @@ -168,6 +169,7 @@ pub const LINUX_LABELS: Labels = Labels { "zfs", "crypto_LUKS", "LVM2_member", + "zfs_member", ]), }; From 6524265cada6d9b3cfd0825ccfc2f7254e4fb900 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 20:39:28 +0200 Subject: [PATCH 26/64] error handling must be improved --- anylinuxfs/src/main.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index be038ae..034611d 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2250,23 +2250,29 @@ impl AppRunner { let vers4 = mnt_dev_info.fs_type() == Some("zfs"); if vers4 { let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); - if let Err(e) = fs::create_dir_all(&mnt_point) { - host_eprintln!( - "Failed to create mount point {}: {:#}", - mnt_point.display(), - e - ); - } else if let Err(e) = chown( + fs::create_dir_all(&mnt_point).map_err(|e| { + StatusError::new( + &format!( + "Failed to create mount point {}: {:#}", + mnt_point.display(), + e + ), + 1, + ) + })?; + + chown( &mnt_point, Some(config.common.invoker_uid), Some(config.common.invoker_gid), - ) { - host_eprintln!( - "Failed to change owner of {}: {:#}", - mnt_point.display(), - e - ); - } + ) + .map_err(|e| { + StatusError::new( + &format!("Failed to change owner of {}: {:#}", mnt_point.display(), e), + 1, + ) + })?; + config.custom_mount_point = Some(mnt_point); } let mount_result = mount_nfs(&share_path, &config, vers4); From 31d775cc83fc6381eada352679cbf95ade1abe68 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 20:41:54 +0200 Subject: [PATCH 27/64] always use zfs_root label --- vmproxy/src/main.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 1ebcf84..eb6cdda 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -404,20 +404,10 @@ fn run() -> anyhow::Result<()> { let name = &cli.mount_name; let mount_name = if !is_logical { if is_zfs { - let label = { - script("modprobe zfs") - .status() - .context("Failed to load zfs module")?; - - let zpools = zfs::get_importable_zpools()?; - // println!("Importable zpools: {:?}", &zpools); - - if zpools.len() > 1 { - "zfs_root".to_owned() - } else { - zpools[0].name.clone() - } - }; + script("modprobe zfs") + .status() + .context("Failed to load zfs module")?; + let label = "zfs_root".to_owned(); println!("", &label); label } else { From 8cfbaa20fe8c6157508165bc2f7f655e41dbfc6b Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 20:56:23 +0200 Subject: [PATCH 28/64] we actually don't want StatusError here --- anylinuxfs/src/main.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 034611d..1d14856 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2250,15 +2250,8 @@ impl AppRunner { let vers4 = mnt_dev_info.fs_type() == Some("zfs"); if vers4 { let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); - fs::create_dir_all(&mnt_point).map_err(|e| { - StatusError::new( - &format!( - "Failed to create mount point {}: {:#}", - mnt_point.display(), - e - ), - 1, - ) + fs::create_dir_all(&mnt_point).with_context(|| { + format!("Failed to create mount point {}", mnt_point.display()) })?; chown( @@ -2266,11 +2259,8 @@ impl AppRunner { Some(config.common.invoker_uid), Some(config.common.invoker_gid), ) - .map_err(|e| { - StatusError::new( - &format!("Failed to change owner of {}: {:#}", mnt_point.display(), e), - 1, - ) + .with_context(|| { + format!("Failed to change owner of {}", mnt_point.display()) })?; config.custom_mount_point = Some(mnt_point); From b3b31fc10a39ec86c1a8c35aacdc9a4aecd69ea6 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 21:29:38 +0200 Subject: [PATCH 29/64] fix read-only zfs mount --- vmproxy/src/main.rs | 11 ++++++----- vmproxy/src/zfs.rs | 15 +++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index eb6cdda..bc04f38 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -306,6 +306,11 @@ fn run() -> anyhow::Result<()> { let mount_options = cli.mount_options; let verbose = cli.verbose; + let specified_read_only = mount_options + .as_deref() + .map(|opts| is_read_only_set(opts.split(','))) + .unwrap_or(false); + let (mapper_ident_prefix, cryptsetup_op) = match fs_type.as_deref() { Some("crypto_LUKS") => ("luks", "open"), Some("BitLocker") => ("btlk", "bitlkOpen"), @@ -487,7 +492,7 @@ fn run() -> anyhow::Result<()> { // .context(format!("Failed to mount '/dev/vda' on '{}'", &mount_point))?; let zfs_mountpoints = if is_zfs { - let (status, mountpoints) = zfs::import_all_zpools(&mount_point)?; + let (status, mountpoints) = zfs::import_all_zpools(&mount_point, specified_read_only)?; if !status.success() { return Err(anyhow!( "Importing zpools failed with error code {}", @@ -622,10 +627,6 @@ fn run() -> anyhow::Result<()> { // list_dir(mount_point); - let specified_read_only = mount_options - .as_deref() - .map(|opts| is_read_only_set(opts.split(','))) - .unwrap_or(false); let effective_read_only = is_read_only_set(effective_mount_options.iter().map(String::as_str)); if specified_read_only != effective_read_only { diff --git a/vmproxy/src/zfs.rs b/vmproxy/src/zfs.rs index 97b071c..ed0f7bd 100644 --- a/vmproxy/src/zfs.rs +++ b/vmproxy/src/zfs.rs @@ -126,10 +126,17 @@ fn mountpoints_from_json(text: &str) -> anyhow::Result> { Ok(res) } -pub fn import_all_zpools(mount_point_root: &str) -> anyhow::Result<(ExitStatus, Vec)> { - let res = script(&format!("zpool import -faNR {}", &mount_point_root)) - .status() - .context("Failed to run zpool import command")?; +pub fn import_all_zpools( + mount_point_root: &str, + read_only: bool, +) -> anyhow::Result<(ExitStatus, Vec)> { + let opts = if read_only { "-o readonly=on" } else { "" }; + let res = script(&format!( + "zpool import {} -faNR {}", + opts, &mount_point_root + )) + .status() + .context("Failed to run zpool import command")?; if !res.success() { return Ok((res, Vec::new())); From f3bd742f9f250124ea8234988d84bcd3187cbfd0 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 21:37:28 +0200 Subject: [PATCH 30/64] fix read-only mode export --- vmproxy/src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index bc04f38..ca426e0 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -627,7 +627,13 @@ fn run() -> anyhow::Result<()> { // list_dir(mount_point); - let effective_read_only = is_read_only_set(effective_mount_options.iter().map(String::as_str)); + let effective_read_only = if is_zfs { + // we don't check effective ro flag for ZFS + // (it's only useful for NTFS in hibernation anyway) + specified_read_only + } else { + is_read_only_set(effective_mount_options.iter().map(String::as_str)) + }; if specified_read_only != effective_read_only { println!(""); From b354aae232591a88c4ae081343c1534843d2c23c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Thu, 25 Sep 2025 21:43:51 +0200 Subject: [PATCH 31/64] make sure each fsid is unique --- anylinuxfs/src/main.rs | 1 - vmproxy/src/main.rs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 1d14856..14619d7 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -169,7 +169,6 @@ impl CustomActionEnvironment for CustomActionConfig { let mut undefined_vars = Vec::new(); let mut env_vars = Vec::new(); for var_str in &self.environment { - // TODO: format could be validated here let var_name = var_str .split('=') .next() diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index ca426e0..7ebc5e0 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -240,7 +240,7 @@ fn statfs(path: impl AsRef) -> io::Result { Ok(buf) } -fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result { +fn export_args_for_path(path: &str, export_mode: &str, fsid: usize) -> anyhow::Result { let mut export_args = format!("{export_mode},no_subtree_check,no_root_squash,insecure"); if statfs(path) .with_context(|| format!("statfs failed for {path}"))? @@ -248,7 +248,7 @@ fn export_args_for_path(path: &str, export_mode: &str) -> anyhow::Result == 0x65735546 { // exporting FUSE requires fsid - export_args += ",fsid=728" // TODO: unique fsid + export_args += &format!(",fsid={}", fsid) } Ok(export_args) } @@ -654,14 +654,14 @@ fn run() -> anyhow::Result<()> { } let mut exports = vec![]; - for p in paths { - let a = export_args_for_path(&p, export_mode)?; + for (i, p) in paths.into_iter().enumerate() { + let a = export_args_for_path(&p, export_mode, i)?; exports.push((p, a)); } exports } else { // single export - let export_args = export_args_for_path(&export_path, export_mode)?; + let export_args = export_args_for_path(&export_path, export_mode, 0)?; vec![(export_path, export_args)] }; let mut exports_content = String::new(); From 825ba63c39acc4e03f226b7b7c9ad58e455457b1 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 21:17:39 +0200 Subject: [PATCH 32/64] disable NFSv4 --- anylinuxfs/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 14619d7..48f8cf8 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2246,7 +2246,8 @@ impl AppRunner { } _ => format!("/mnt/{share_name}"), }; - let vers4 = mnt_dev_info.fs_type() == Some("zfs"); + // let vers4 = mnt_dev_info.fs_type() == Some("zfs"); + let vers4 = false; if vers4 { let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); fs::create_dir_all(&mnt_point).with_context(|| { From 34a0dd66ac2d6615b575d06354a855751c6abc1a Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 22:25:35 +0200 Subject: [PATCH 33/64] collect list of all NFS exports from the VM --- anylinuxfs/src/main.rs | 65 +++++++++++++++++++++++++++++++----------- vmproxy/src/main.rs | 3 +- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 48f8cf8..d9c9113 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1178,13 +1178,21 @@ fn start_gvproxy(config: &Config) -> anyhow::Result { #[derive(Debug)] enum NfsStatus { - Ready(Option, Option, bool), + Ready(NfsReadyState), Failed(Option), } +#[derive(Debug)] +struct NfsReadyState { + fslabel: Option, + fstype: Option, + changed_to_ro: bool, + exports: Vec, +} + impl NfsStatus { fn ok(&self) -> bool { - matches!(self, NfsStatus::Ready(_, _, _)) + matches!(self, NfsStatus::Ready(_)) } } @@ -2113,6 +2121,7 @@ impl AppRunner { let mut buf_reader = PassthroughBufReader::new(unsafe { File::from_raw_fd(pty_fd) }); let mut line = String::new(); + let mut exports = Vec::new(); loop { let bytes = match buf_reader.read_line(&mut line) { @@ -2128,11 +2137,12 @@ impl AppRunner { if line.contains("READY AND WAITING FOR NFS CLIENT CONNECTIONS") { // Notify the main thread that NFS server is ready nfs_ready_tx - .send(NfsStatus::Ready( - fslabel.take(), - fstype.take(), + .send(NfsStatus::Ready(NfsReadyState { + fslabel: fslabel.take(), + fstype: fstype.take(), changed_to_ro, - )) + exports: exports.clone(), + })) .unwrap(); nfs_ready = true; } else if line.starts_with("") { changed_to_ro = true; + } else if line.starts_with("") + .unwrap_or(pattern) + .to_string() + }) { + exports.push(export_path); + } } else if line.starts_with("") { vm_pwd_prompt_tx.send(true).unwrap(); } else if line.starts_with("") { @@ -2218,18 +2238,24 @@ impl AppRunner { let signals = signal_hub.subscribe(); let event_session = diskutil::EventSession::new(signals)?; - if let NfsStatus::Ready(Some(label), _, _) = &nfs_status { - mnt_dev_info.set_label(label); - rt_info.lock().unwrap().dev_info.set_label(label); - } + if let NfsStatus::Ready(NfsReadyState { + fslabel, + fstype, + changed_to_ro, + exports, + }) = &nfs_status + { + if let Some(label) = fslabel { + mnt_dev_info.set_label(label); + rt_info.lock().unwrap().dev_info.set_label(label); + } - if let NfsStatus::Ready(_, Some(fstype), _) = &nfs_status { - mnt_dev_info.set_fs_type(fstype); - rt_info.lock().unwrap().dev_info.set_fs_type(fstype); - } + if let Some(fstype) = fstype { + mnt_dev_info.set_fs_type(fstype); + rt_info.lock().unwrap().dev_info.set_fs_type(fstype); + } - if let NfsStatus::Ready(_, _, changed_to_ro) = nfs_status { - if changed_to_ro { + if *changed_to_ro { rt_info.lock().unwrap().mount_config.read_only = true; let mount_opts = rt_info.lock().unwrap().mount_config.mount_options.clone(); let new_mount_opts = mount_opts @@ -2237,6 +2263,13 @@ impl AppRunner { .unwrap_or("ro".into()); rt_info.lock().unwrap().mount_config.mount_options = Some(new_mount_opts); } + + if !exports.is_empty() { + host_println!("NFS exports:"); + for export in exports { + host_println!(" {}", export); + } + } } let share_name = mnt_dev_info.auto_mount_name(); diff --git a/vmproxy/src/main.rs b/vmproxy/src/main.rs index 7ebc5e0..506512f 100644 --- a/vmproxy/src/main.rs +++ b/vmproxy/src/main.rs @@ -667,7 +667,8 @@ fn run() -> anyhow::Result<()> { let mut exports_content = String::new(); for (export_path, export_args) in &all_exports { - exports_content += &format!("\"{}\" *({})\n", &export_path, export_args); + println!("", export_path); + exports_content += &format!("\"{}\" *({})\n", export_path, export_args); } fs::write("/etc/exports", exports_content).context("Failed to write to /etc/exports")?; From ba5b573d1f87eb9b8b3f57f60c458c8597e21384 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 22:34:23 +0200 Subject: [PATCH 34/64] make the exports list sorted --- anylinuxfs/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index d9c9113..e2173f0 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2121,7 +2121,7 @@ impl AppRunner { let mut buf_reader = PassthroughBufReader::new(unsafe { File::from_raw_fd(pty_fd) }); let mut line = String::new(); - let mut exports = Vec::new(); + let mut exports = BTreeSet::new(); loop { let bytes = match buf_reader.read_line(&mut line) { @@ -2141,7 +2141,7 @@ impl AppRunner { fslabel: fslabel.take(), fstype: fstype.take(), changed_to_ro, - exports: exports.clone(), + exports: exports.iter().cloned().collect(), })) .unwrap(); nfs_ready = true; @@ -2184,7 +2184,7 @@ impl AppRunner { .unwrap_or(pattern) .to_string() }) { - exports.push(export_path); + exports.insert(export_path); } } else if line.starts_with("") { vm_pwd_prompt_tx.send(true).unwrap(); From 1497e4007b50dd0f32bcef750fc5805957e3c214 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 23:06:23 +0200 Subject: [PATCH 35/64] automatically mount all NFS exports --- anylinuxfs/src/main.rs | 97 ++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index e2173f0..fb4fb51 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1220,6 +1220,36 @@ fn wait_for_nfs_server( Ok(nfs_ready) } +fn mount_nfs_subdirs<'a>( + share_paths: impl Iterator, + mnt_point_base: impl AsRef, +) -> anyhow::Result<()> { + for path in share_paths { + let shell_script = format!( + "mount -t nfs \"localhost:{}\" \"{}\"", + path, + mnt_point_base.as_ref().display() + ); + let status = Command::new("sh") + .arg("-c") + .arg(&shell_script) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + if !status.success() { + return Err(anyhow!( + "mount failed with exit code {}", + status + .code() + .map(|c| c.to_string()) + .unwrap_or("unknown".to_owned()) + )); + } + } + Ok(()) +} + fn mount_nfs(share_path: &str, config: &MountConfig, vers4: bool) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { let opts = if vers4 { "-o vers=4" } else { "" }; @@ -2224,7 +2254,14 @@ impl AppRunner { let nfs_status = wait_for_nfs_server(2049, nfs_ready_rx).unwrap_or(NfsStatus::Failed(None)); - if nfs_status.ok() { + + if let NfsStatus::Ready(NfsReadyState { + fslabel, + fstype, + changed_to_ro, + exports, + }) = &nfs_status + { host_println!("Port 2049 open, NFS server ready"); // from now on, if anything fails, we need to send quit command to the VM @@ -2238,38 +2275,23 @@ impl AppRunner { let signals = signal_hub.subscribe(); let event_session = diskutil::EventSession::new(signals)?; - if let NfsStatus::Ready(NfsReadyState { - fslabel, - fstype, - changed_to_ro, - exports, - }) = &nfs_status - { - if let Some(label) = fslabel { - mnt_dev_info.set_label(label); - rt_info.lock().unwrap().dev_info.set_label(label); - } - - if let Some(fstype) = fstype { - mnt_dev_info.set_fs_type(fstype); - rt_info.lock().unwrap().dev_info.set_fs_type(fstype); - } + if let Some(label) = fslabel { + mnt_dev_info.set_label(label); + rt_info.lock().unwrap().dev_info.set_label(label); + } - if *changed_to_ro { - rt_info.lock().unwrap().mount_config.read_only = true; - let mount_opts = rt_info.lock().unwrap().mount_config.mount_options.clone(); - let new_mount_opts = mount_opts - .map(|opts| format!("ro,{}", opts)) - .unwrap_or("ro".into()); - rt_info.lock().unwrap().mount_config.mount_options = Some(new_mount_opts); - } + if let Some(fstype) = fstype { + mnt_dev_info.set_fs_type(fstype); + rt_info.lock().unwrap().dev_info.set_fs_type(fstype); + } - if !exports.is_empty() { - host_println!("NFS exports:"); - for export in exports { - host_println!(" {}", export); - } - } + if *changed_to_ro { + rt_info.lock().unwrap().mount_config.read_only = true; + let mount_opts = rt_info.lock().unwrap().mount_config.mount_options.clone(); + let new_mount_opts = mount_opts + .map(|opts| format!("ro,{}", opts)) + .unwrap_or("ro".into()); + rt_info.lock().unwrap().mount_config.mount_options = Some(new_mount_opts); } let share_name = mnt_dev_info.auto_mount_name(); @@ -2304,6 +2326,19 @@ impl AppRunner { Err(e) => host_eprintln!("Failed to request NFS mount: {:#}", e), }; + let additional_exports = exports + .iter() + .map(|item| item.as_str()) + .filter(|&export_path| export_path != &share_path); + + let mnt_point_base = config + .custom_mount_point + .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); + match mount_nfs_subdirs(additional_exports, mnt_point_base) { + Ok(_) => {} + Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), + } + // drop privileges back to the original user if he used sudo drop_privileges(config.common.sudo_uid, config.common.sudo_gid)?; From e6a885a252cdab487d52eda933cfdd0cc0795a5c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 23:10:21 +0200 Subject: [PATCH 36/64] fix order of mount operations --- anylinuxfs/src/main.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index fb4fb51..2dd9e12 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1233,8 +1233,8 @@ fn mount_nfs_subdirs<'a>( let status = Command::new("sh") .arg("-c") .arg(&shell_script) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + // .stdout(Stdio::null()) + // .stderr(Stdio::null()) .status()?; if !status.success() { @@ -2326,22 +2326,6 @@ impl AppRunner { Err(e) => host_eprintln!("Failed to request NFS mount: {:#}", e), }; - let additional_exports = exports - .iter() - .map(|item| item.as_str()) - .filter(|&export_path| export_path != &share_path); - - let mnt_point_base = config - .custom_mount_point - .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); - match mount_nfs_subdirs(additional_exports, mnt_point_base) { - Ok(_) => {} - Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), - } - - // drop privileges back to the original user if he used sudo - drop_privileges(config.common.sudo_uid, config.common.sudo_gid)?; - let mount_point_opt = if mount_result.is_ok() { let nfs_path = PathBuf::from(format!("localhost:{}", share_path)); event_session.wait_for_mount(&nfs_path) @@ -2357,8 +2341,24 @@ impl AppRunner { ); rt_info.lock().unwrap().mount_point = Some(mount_point.display().into()); + + let additional_exports = exports + .iter() + .map(|item| item.as_str()) + .filter(|&export_path| export_path != &share_path); + + let mnt_point_base = config + .custom_mount_point + .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); + match mount_nfs_subdirs(additional_exports, mnt_point_base) { + Ok(_) => {} + Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), + } } + // drop privileges back to the original user if he used sudo + drop_privileges(config.common.sudo_uid, config.common.sudo_gid)?; + if can_detach { // tell the parent to detach from console (i.e. exit) unsafe { write_to_pipe(comm_write_fd, b"detach\n") } From a4c56777c8d2ee7551ddada2b691cd76f59441b0 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 23:23:24 +0200 Subject: [PATCH 37/64] fix subdir mount paths --- anylinuxfs/src/main.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 2dd9e12..b4ee91e 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1221,21 +1221,23 @@ fn wait_for_nfs_server( } fn mount_nfs_subdirs<'a>( - share_paths: impl Iterator, + share_path_base: &str, + subdirs: impl Iterator, mnt_point_base: impl AsRef, ) -> anyhow::Result<()> { - for path in share_paths { + for subdir in subdirs { + let subdir_relative = subdir.trim_start_matches(share_path_base); let shell_script = format!( "mount -t nfs \"localhost:{}\" \"{}\"", - path, - mnt_point_base.as_ref().display() + subdir, + mnt_point_base.as_ref().join(subdir_relative).display() ); let status = Command::new("sh") .arg("-c") .arg(&shell_script) // .stdout(Stdio::null()) // .stderr(Stdio::null()) - .status()?; + .status()?; // TODO: make sure any error is properly printed if !status.success() { return Err(anyhow!( @@ -2350,7 +2352,7 @@ impl AppRunner { let mnt_point_base = config .custom_mount_point .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); - match mount_nfs_subdirs(additional_exports, mnt_point_base) { + match mount_nfs_subdirs(&share_path, additional_exports, mnt_point_base) { Ok(_) => {} Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), } From 058abd620e2083c475201b0bcf8b8286f64e29af Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 23:25:59 +0200 Subject: [PATCH 38/64] debug nfs mount commands --- anylinuxfs/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index b4ee91e..1c8e43d 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1232,6 +1232,7 @@ fn mount_nfs_subdirs<'a>( subdir, mnt_point_base.as_ref().join(subdir_relative).display() ); + host_println!("Running NFS mount command: `{}`", &shell_script); let status = Command::new("sh") .arg("-c") .arg(&shell_script) From 4b0618181089dd1318ef738967df76720aaf6f79 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Fri, 17 Oct 2025 23:32:44 +0200 Subject: [PATCH 39/64] trim leading slash from subdir_relative --- anylinuxfs/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 1c8e43d..4eefcea 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1226,7 +1226,9 @@ fn mount_nfs_subdirs<'a>( mnt_point_base: impl AsRef, ) -> anyhow::Result<()> { for subdir in subdirs { - let subdir_relative = subdir.trim_start_matches(share_path_base); + let subdir_relative = subdir + .trim_start_matches(share_path_base) + .trim_start_matches('/'); let shell_script = format!( "mount -t nfs \"localhost:{}\" \"{}\"", subdir, From e1fdc292a09f7f24e029dd2f5747648bd137e6c3 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 09:44:03 +0200 Subject: [PATCH 40/64] construct a trie from NFS subdirs --- anylinuxfs/src/fsutil.rs | 86 ++++++++++++++++++++++++++++++++++++++++ anylinuxfs/src/main.rs | 36 +---------------- 2 files changed, 87 insertions(+), 35 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 53405c2..2457c36 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -1,9 +1,12 @@ +use anyhow::anyhow; +use common_utils::host_println; use std::{ collections::HashSet, ffi::{CStr, CString, OsStr, OsString}, io, mem, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, + process::Command, ptr::null_mut, }; @@ -81,3 +84,86 @@ fn os_str_from_c_chars(chars: &[i8]) -> &OsStr { let cstr = unsafe { CStr::from_ptr(chars.as_ptr()) }; OsStr::from_bytes(cstr.to_bytes()) } + +mod dirtrie { + use std::{collections::BTreeMap, ffi::OsString, fmt::Display, path::Path}; + + #[derive(Debug, Default)] + pub struct Node { + pub children: BTreeMap, + } + + impl Node { + pub fn insert(&mut self, path: &Path) { + let mut current = self; + for segment in path.components() { + let segment = segment.as_os_str().to_owned(); + current = current.children.entry(segment).or_default(); + } + } + } + + impl Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt_node( + node: &Node, + f: &mut std::fmt::Formatter<'_>, + prefix: &str, + ) -> std::fmt::Result { + for (segment, child) in &node.children { + write!(f, "{}{}\r\n", prefix, segment.to_string_lossy())?; + fmt_node(child, f, &format!("{}--", prefix))?; + } + Ok(()) + } + fmt_node(self, f, "") + } + } +} + +pub fn mount_nfs_subdirs<'a>( + share_path_base: &str, + subdirs: impl Iterator, + mnt_point_base: impl AsRef, +) -> anyhow::Result<()> { + let mut trie = dirtrie::Node::default(); + // TODO: try if mounting in parallel is faster + // but make sure the order is correct: + // - we'd need to construct a trie of all subdirs + // where each node corresponds to a path segment + // - each node mounts its own subdir prefix path + // - then repeats recursively for all children at once + for subdir in subdirs { + let subdir_relative = subdir + .trim_start_matches(share_path_base) + .trim_start_matches('/'); + + trie.insert(Path::new(subdir_relative)); + + let shell_script = format!( + "mount -t nfs \"localhost:{}\" \"{}\"", + subdir, + mnt_point_base.as_ref().join(subdir_relative).display() + ); + // host_println!("Running NFS mount command: `{}`", &shell_script); + // TODO: elevate if needed (e.g. mounting image under /Volumes) + let status = Command::new("sh") + .arg("-c") + .arg(&shell_script) + // .stdout(Stdio::null()) + // .stderr(Stdio::null()) + .status()?; // TODO: make sure any error is properly printed + + if !status.success() { + return Err(anyhow!( + "mount failed with exit code {}", + status + .code() + .map(|c| c.to_string()) + .unwrap_or("unknown".to_owned()) + )); + } + } + host_println!("Mounted NFS subdirectories:\n{}", trie); + Ok(()) +} diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 4eefcea..d512df4 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -39,6 +39,7 @@ use utils::{ OutputAction, PassthroughBufReader, StatusError, write_to_pipe, }; +use crate::fsutil::mount_nfs_subdirs; use crate::utils::{ToCStringVec, ToPtrVec}; mod api; @@ -1220,41 +1221,6 @@ fn wait_for_nfs_server( Ok(nfs_ready) } -fn mount_nfs_subdirs<'a>( - share_path_base: &str, - subdirs: impl Iterator, - mnt_point_base: impl AsRef, -) -> anyhow::Result<()> { - for subdir in subdirs { - let subdir_relative = subdir - .trim_start_matches(share_path_base) - .trim_start_matches('/'); - let shell_script = format!( - "mount -t nfs \"localhost:{}\" \"{}\"", - subdir, - mnt_point_base.as_ref().join(subdir_relative).display() - ); - host_println!("Running NFS mount command: `{}`", &shell_script); - let status = Command::new("sh") - .arg("-c") - .arg(&shell_script) - // .stdout(Stdio::null()) - // .stderr(Stdio::null()) - .status()?; // TODO: make sure any error is properly printed - - if !status.success() { - return Err(anyhow!( - "mount failed with exit code {}", - status - .code() - .map(|c| c.to_string()) - .unwrap_or("unknown".to_owned()) - )); - } - } - Ok(()) -} - fn mount_nfs(share_path: &str, config: &MountConfig, vers4: bool) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { let opts = if vers4 { "-o vers=4" } else { "" }; From e0592b7d8f4ec7be4333f8055a3e11687f063a11 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 09:59:14 +0200 Subject: [PATCH 41/64] formatting --- anylinuxfs/src/fsutil.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 2457c36..d1bd97a 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -164,6 +164,6 @@ pub fn mount_nfs_subdirs<'a>( )); } } - host_println!("Mounted NFS subdirectories:\n{}", trie); + host_println!("Mounted NFS subdirectories:\r\n{}", trie); Ok(()) } From 8218f28894c7814adae8504644d79efed49220e1 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 10:11:59 +0200 Subject: [PATCH 42/64] store absolute mount point paths in the trie --- anylinuxfs/src/fsutil.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index d1bd97a..adf5d32 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -90,16 +90,18 @@ mod dirtrie { #[derive(Debug, Default)] pub struct Node { + pub mount_point: Option, pub children: BTreeMap, } impl Node { - pub fn insert(&mut self, path: &Path) { + pub fn insert(&mut self, path: &Path, full_path: &str) { let mut current = self; for segment in path.components() { let segment = segment.as_os_str().to_owned(); current = current.children.entry(segment).or_default(); } + current.mount_point = Some(full_path.to_owned()); } } @@ -111,7 +113,13 @@ mod dirtrie { prefix: &str, ) -> std::fmt::Result { for (segment, child) in &node.children { - write!(f, "{}{}\r\n", prefix, segment.to_string_lossy())?; + write!( + f, + "{}{} ({})\r\n", + prefix, + segment.to_string_lossy(), + child.mount_point.as_deref().unwrap_or("") + )?; fmt_node(child, f, &format!("{}--", prefix))?; } Ok(()) @@ -121,6 +129,13 @@ mod dirtrie { } } +// fn parallel_mount_recursive( +// mnt_point_base: impl AsRef, +// trie: &dirtrie::Node, +// ) -> anyhow::Result<()> { +// Ok(()) +// } + pub fn mount_nfs_subdirs<'a>( share_path_base: &str, subdirs: impl Iterator, @@ -138,7 +153,7 @@ pub fn mount_nfs_subdirs<'a>( .trim_start_matches(share_path_base) .trim_start_matches('/'); - trie.insert(Path::new(subdir_relative)); + trie.insert(Path::new(subdir_relative), subdir); let shell_script = format!( "mount -t nfs \"localhost:{}\" \"{}\"", From 6cff3808f2a7f6dd67c499cb8ec9a6eefcb4da73 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 10:34:19 +0200 Subject: [PATCH 43/64] implemented parallel mount --- anylinuxfs/Cargo.lock | 46 +++++++++++++++++++++++++ anylinuxfs/Cargo.toml | 1 + anylinuxfs/src/fsutil.rs | 73 ++++++++++++++++++++++------------------ 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/anylinuxfs/Cargo.lock b/anylinuxfs/Cargo.lock index 06ab262..5872081 100644 --- a/anylinuxfs/Cargo.lock +++ b/anylinuxfs/Cargo.lock @@ -89,6 +89,7 @@ dependencies = [ "objc2-system-configuration", "os_socketaddr", "plist", + "rayon", "regex", "rpassword", "serde", @@ -259,6 +260,31 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.29.0" @@ -1040,6 +1066,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.12" diff --git a/anylinuxfs/Cargo.toml b/anylinuxfs/Cargo.toml index 12b4122..7747126 100644 --- a/anylinuxfs/Cargo.toml +++ b/anylinuxfs/Cargo.toml @@ -30,6 +30,7 @@ if-addrs = "0.13.4" crossterm = "0.29.0" objc2-system-configuration = "0.3.1" os_socketaddr = "0.2.5" +rayon = "1.11.0" [patch.crates-io] libblkid-rs = { git = 'https://github.com/stratis-storage/libblkid-rs.git', rev = "5c08342" } diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index adf5d32..9e2a892 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use common_utils::host_println; +use rayon::prelude::*; use std::{ collections::HashSet, ffi::{CStr, CString, OsStr, OsString}, @@ -90,7 +91,7 @@ mod dirtrie { #[derive(Debug, Default)] pub struct Node { - pub mount_point: Option, + pub paths: Option<(OsString, String)>, pub children: BTreeMap, } @@ -101,7 +102,7 @@ mod dirtrie { let segment = segment.as_os_str().to_owned(); current = current.children.entry(segment).or_default(); } - current.mount_point = Some(full_path.to_owned()); + current.paths = Some((path.as_os_str().to_owned(), full_path.to_owned())); } } @@ -118,7 +119,11 @@ mod dirtrie { "{}{} ({})\r\n", prefix, segment.to_string_lossy(), - child.mount_point.as_deref().unwrap_or("") + child + .paths + .as_ref() + .map(|(_, p)| p.clone()) + .unwrap_or("".to_owned()) )?; fmt_node(child, f, &format!("{}--", prefix))?; } @@ -129,38 +134,14 @@ mod dirtrie { } } -// fn parallel_mount_recursive( -// mnt_point_base: impl AsRef, -// trie: &dirtrie::Node, -// ) -> anyhow::Result<()> { -// Ok(()) -// } - -pub fn mount_nfs_subdirs<'a>( - share_path_base: &str, - subdirs: impl Iterator, - mnt_point_base: impl AsRef, -) -> anyhow::Result<()> { - let mut trie = dirtrie::Node::default(); - // TODO: try if mounting in parallel is faster - // but make sure the order is correct: - // - we'd need to construct a trie of all subdirs - // where each node corresponds to a path segment - // - each node mounts its own subdir prefix path - // - then repeats recursively for all children at once - for subdir in subdirs { - let subdir_relative = subdir - .trim_start_matches(share_path_base) - .trim_start_matches('/'); - - trie.insert(Path::new(subdir_relative), subdir); - +fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> anyhow::Result<()> { + if let Some((rel_path, nfs_path)) = &trie.paths { let shell_script = format!( "mount -t nfs \"localhost:{}\" \"{}\"", - subdir, - mnt_point_base.as_ref().join(subdir_relative).display() + nfs_path, + mnt_point_base.join(rel_path).display() ); - // host_println!("Running NFS mount command: `{}`", &shell_script); + host_println!("Running NFS mount command: `{}`", &shell_script); // TODO: elevate if needed (e.g. mounting image under /Volumes) let status = Command::new("sh") .arg("-c") @@ -179,6 +160,34 @@ pub fn mount_nfs_subdirs<'a>( )); } } + trie.children + .par_iter() + .try_for_each(|(_, child)| parallel_mount_recursive(mnt_point_base.clone(), child))?; + + Ok(()) +} + +pub fn mount_nfs_subdirs<'a>( + share_path_base: &str, + subdirs: impl Iterator, + mnt_point_base: impl AsRef, +) -> anyhow::Result<()> { + let mut trie = dirtrie::Node::default(); + // TODO: try if mounting in parallel is faster + // but make sure the order is correct: + // - we'd need to construct a trie of all subdirs + // where each node corresponds to a path segment + // - each node mounts its own subdir prefix path + // - then repeats recursively for all children at once + for subdir in subdirs { + let subdir_relative = subdir + .trim_start_matches(share_path_base) + .trim_start_matches('/'); + + trie.insert(Path::new(subdir_relative), subdir); + } + + parallel_mount_recursive(mnt_point_base.as_ref().into(), &trie)?; host_println!("Mounted NFS subdirectories:\r\n{}", trie); Ok(()) } From 233d29be108c8958b33747bed2793226f6236f8f Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 10:42:30 +0200 Subject: [PATCH 44/64] only log the mount point paths --- anylinuxfs/src/fsutil.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 9e2a892..21fea16 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -141,7 +141,8 @@ fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> an nfs_path, mnt_point_base.join(rel_path).display() ); - host_println!("Running NFS mount command: `{}`", &shell_script); + // host_println!("Running NFS mount command: `{}`", &shell_script); + // TODO: elevate if needed (e.g. mounting image under /Volumes) let status = Command::new("sh") .arg("-c") @@ -159,6 +160,10 @@ fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> an .unwrap_or("unknown".to_owned()) )); } + host_println!( + "Mounted subdirectory: {}", + mnt_point_base.join(rel_path).display() + ); } trie.children .par_iter() @@ -188,6 +193,6 @@ pub fn mount_nfs_subdirs<'a>( } parallel_mount_recursive(mnt_point_base.as_ref().into(), &trie)?; - host_println!("Mounted NFS subdirectories:\r\n{}", trie); + // host_println!("Mounted NFS subdirectories:\r\n{}", trie); Ok(()) } From 7dc18b5ff2ef8f3c6569d3dd15eae42abafbba66 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 10:49:57 +0200 Subject: [PATCH 45/64] removed TODO comment --- anylinuxfs/src/fsutil.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 21fea16..aa2f25a 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -178,12 +178,7 @@ pub fn mount_nfs_subdirs<'a>( mnt_point_base: impl AsRef, ) -> anyhow::Result<()> { let mut trie = dirtrie::Node::default(); - // TODO: try if mounting in parallel is faster - // but make sure the order is correct: - // - we'd need to construct a trie of all subdirs - // where each node corresponds to a path segment - // - each node mounts its own subdir prefix path - // - then repeats recursively for all children at once + for subdir in subdirs { let subdir_relative = subdir .trim_start_matches(share_path_base) From 66df8c70100e2f6685c6fe9ea8806dc7c05d9de4 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 11:43:22 +0200 Subject: [PATCH 46/64] unmount command (wip) --- anylinuxfs/src/fsutil.rs | 12 ++++++- anylinuxfs/src/main.rs | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index aa2f25a..2b91434 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -14,6 +14,7 @@ use std::{ #[derive(Debug, Clone)] pub struct MountTable { disks: HashSet, + mount_points: HashSet, } impl MountTable { @@ -36,6 +37,7 @@ impl MountTable { } let mut disks = HashSet::new(); + let mut mount_points = HashSet::new(); for buf in mounts_raw { let mntfromname = os_str_from_c_chars(&buf.f_mntfromname).to_owned(); let mntonname = os_str_from_c_chars(&buf.f_mntonname).to_owned(); @@ -44,15 +46,23 @@ impl MountTable { if !mntfromname.is_empty() && !mntonname.is_empty() { disks.insert(mntfromname); + mount_points.insert(mntonname); } } - Ok(MountTable { disks }) + Ok(MountTable { + disks, + mount_points, + }) } pub fn is_mounted(&self, path: impl AsRef) -> bool { let path = path.as_ref(); self.disks.contains(path.as_os_str()) } + + pub fn mount_points(&self) -> impl Iterator { + self.mount_points.iter() + } } pub fn mounted_from(path: impl AsRef) -> io::Result { diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index d512df4..281ff18 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -408,6 +408,8 @@ Supported partition schemes: - MBR - disk without partitions (single filesystem or LVM/LUKS container).")] Mount(MountCmd), + /// Unmount a filesystem + Unmount(UnmountCmd), /// Init Linux rootfs (can be used to reinitialize virtual environment) Init, /// Show status information (mount parameters, vm resources, etc.) @@ -476,6 +478,13 @@ struct MountCmd { verbose: bool, } +#[derive(Args)] +struct UnmountCmd { + /// Disk identifier or mount point (unmounts all if not specified) + #[arg(id = "DISK_IDENT|MOUNT_POINT")] + path: Option, +} + #[derive(Args)] struct LogCmd { /// Wait for additional logs to be appended @@ -2413,6 +2422,64 @@ impl AppRunner { Ok(()) } + fn run_unmount(&mut self, cmd: UnmountCmd) -> anyhow::Result<()> { + let resp = api::Client::make_request(api::Request::GetConfig); + + match resp { + Ok(api::Response::Config(rt_info)) => { + let mount_point = match validated_mount_point(&rt_info) { + MountStatus::Mounted(mount_point) => mount_point, + MountStatus::NoLonger => { + eprintln!( + "Drive {} no longer mounted but anylinuxfs is still running; try `anylinuxfs stop`.", + &rt_info.mount_config.disk_path + ); + return Err(StatusError::new("Mount point is not valid", 1).into()); + } + MountStatus::NotYet => { + eprintln!( + "Drive {} not mounted yet, please wait", + &rt_info.mount_config.disk_path + ); + return Ok(()); + } + }; + + println!("specified path: {:?}", &cmd.path); + println!("mounted disk_path: {}", &rt_info.mount_config.disk_path); + println!("mount_point: {}", mount_point.display()); + + let mount_table = fsutil::MountTable::new()?; + let our_mount_points: Vec<_> = mount_table + .mount_points() + .filter(|&mpt| { + mpt.to_string_lossy() + .starts_with(&*mount_point.to_string_lossy()) + }) + .collect(); + + if !our_mount_points.is_empty() { + for mpt in our_mount_points { + host_println!("Unmounting {}", mpt.display()); + // unmount_fs(Path::new(mpt))?; + } + } + } + Err(err) => { + if let Some(err) = err.downcast_ref::() { + match err.kind() { + io::ErrorKind::ConnectionRefused => return Ok(()), + io::ErrorKind::NotFound => return Ok(()), + _ => (), + } + } + return Err(err); + } + } + + Ok(()) + } + fn run_init(&mut self) -> anyhow::Result<()> { let _lock_file = LockFile::new(LOCK_FILE)?.acquire_lock(FlockKind::Exclusive)?; let config = load_config(&CommonArgs::default())?; @@ -2695,6 +2762,7 @@ impl AppRunner { let cli = Cli::try_parse_with_default_cmd()?; match cli.commands { Commands::Mount(cmd) => self.run_mount(cmd), + Commands::Unmount(cmd) => self.run_unmount(cmd), Commands::Init => self.run_init(), Commands::Status => self.run_status(), Commands::Log(cmd) => self.run_log(cmd), From ebc9b3dfb036c8c7369168ea761e1790ab7ea88b Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 12:09:00 +0200 Subject: [PATCH 47/64] added parallel unmount --- anylinuxfs/src/fsutil.rs | 47 ++++++++++++++++++++++++++++++++++++++++ anylinuxfs/src/main.rs | 16 +++++--------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 2b91434..7a283fd 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -201,3 +201,50 @@ pub fn mount_nfs_subdirs<'a>( // host_println!("Mounted NFS subdirectories:\r\n{}", trie); Ok(()) } + +fn parallel_unmount_recursive(trie: &dirtrie::Node) -> anyhow::Result<()> { + trie.children + .par_iter() + .try_for_each(|(_, child)| parallel_unmount_recursive(child))?; + + if let Some((_, mount_path)) = &trie.paths { + let shell_script = format!("diskutil unmount \"{}\"", mount_path); + // host_println!("Running NFS unmount command: `{}`", &shell_script); + let status = Command::new("sh") + .arg("-c") + .arg(&shell_script) + // .stdout(Stdio::null()) + // .stderr(Stdio::null()) + .status()?; // TODO: make sure any error is properly printed + if !status.success() { + return Err(anyhow!( + "umount failed with exit code {}", + status + .code() + .map(|c| c.to_string()) + .unwrap_or("unknown".to_owned()) + )); + } + host_println!("Unmounted subdirectory: {}", mount_path); + } + Ok(()) +} + +pub fn unmount_nfs_subdirs<'a>( + subdirs: impl Iterator, + mnt_point_base: impl AsRef, +) -> anyhow::Result<()> { + let mut trie = dirtrie::Node::default(); + + for subdir in subdirs { + let subdir = subdir.to_string_lossy(); + let subdir_relative = subdir + .trim_start_matches(&*mnt_point_base.as_ref().to_string_lossy()) + .trim_start_matches('/'); + + trie.insert(Path::new(subdir_relative), &subdir); + } + + parallel_unmount_recursive(&trie)?; + Ok(()) +} diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 281ff18..24dca23 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -39,7 +39,7 @@ use utils::{ OutputAction, PassthroughBufReader, StatusError, write_to_pipe, }; -use crate::fsutil::mount_nfs_subdirs; +use crate::fsutil::{mount_nfs_subdirs, unmount_nfs_subdirs}; use crate::utils::{ToCStringVec, ToPtrVec}; mod api; @@ -2450,20 +2450,16 @@ impl AppRunner { println!("mount_point: {}", mount_point.display()); let mount_table = fsutil::MountTable::new()?; - let our_mount_points: Vec<_> = mount_table + // TODO: check if cmd.path corresponds to one of our mount points + let our_mount_points = mount_table .mount_points() + .map(|item| item.as_os_str()) .filter(|&mpt| { mpt.to_string_lossy() .starts_with(&*mount_point.to_string_lossy()) - }) - .collect(); + }); - if !our_mount_points.is_empty() { - for mpt in our_mount_points { - host_println!("Unmounting {}", mpt.display()); - // unmount_fs(Path::new(mpt))?; - } - } + unmount_nfs_subdirs(our_mount_points, mount_point)?; } Err(err) => { if let Some(err) = err.downcast_ref::() { From fb268a6aab56a0cea57bb7bb85397aed808311d5 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 12:15:29 +0200 Subject: [PATCH 48/64] simplified unmount error handling --- anylinuxfs/src/fsutil.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 7a283fd..077269e 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -210,22 +210,8 @@ fn parallel_unmount_recursive(trie: &dirtrie::Node) -> anyhow::Result<()> { if let Some((_, mount_path)) = &trie.paths { let shell_script = format!("diskutil unmount \"{}\"", mount_path); // host_println!("Running NFS unmount command: `{}`", &shell_script); - let status = Command::new("sh") - .arg("-c") - .arg(&shell_script) - // .stdout(Stdio::null()) - // .stderr(Stdio::null()) - .status()?; // TODO: make sure any error is properly printed - if !status.success() { - return Err(anyhow!( - "umount failed with exit code {}", - status - .code() - .map(|c| c.to_string()) - .unwrap_or("unknown".to_owned()) - )); - } - host_println!("Unmounted subdirectory: {}", mount_path); + // exit status ignored, we don't want to exit early if one unmount fails + let _ = Command::new("sh").arg("-c").arg(&shell_script).status()?; } Ok(()) } From 314cc80a03228314b0da089b5dd713c647b15ff9 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 12:40:14 +0200 Subject: [PATCH 49/64] validate unmount path if specified --- anylinuxfs/src/main.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 24dca23..9192984 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2445,12 +2445,24 @@ impl AppRunner { } }; - println!("specified path: {:?}", &cmd.path); - println!("mounted disk_path: {}", &rt_info.mount_config.disk_path); - println!("mount_point: {}", mount_point.display()); + if let Some(path) = &cmd.path { + match fs::canonicalize(path) { + Ok(abs_path) => { + if abs_path != Path::new(&rt_info.mount_config.disk_path) + && abs_path != mount_point + { + println!("The specified path was not mounted by anylinuxfs."); + return Ok(()); + } + } + Err(err) => { + return Err(err).with_context(|| format!("invalid path {}", path)); + } + } + } let mount_table = fsutil::MountTable::new()?; - // TODO: check if cmd.path corresponds to one of our mount points + let our_mount_points = mount_table .mount_points() .map(|item| item.as_os_str()) From bba1e7363da31cb5a21d365cf91d56f3810525e9 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:08:24 +0200 Subject: [PATCH 50/64] helper for properly printing (and logging) child process output --- anylinuxfs/src/fsutil.rs | 19 ++++++++++++------- anylinuxfs/src/main.rs | 25 ++----------------------- anylinuxfs/src/utils.rs | 30 ++++++++++++++++++++++++++++-- common-utils/src/log.rs | 1 + 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 077269e..876c803 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -1,5 +1,5 @@ -use anyhow::anyhow; -use common_utils::host_println; +use anyhow::{Context, anyhow}; +use common_utils::{host_println, log}; use rayon::prelude::*; use std::{ collections::HashSet, @@ -7,10 +7,12 @@ use std::{ io, mem, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, ptr::null_mut, }; +use crate::utils; + #[derive(Debug, Clone)] pub struct MountTable { disks: HashSet, @@ -154,12 +156,15 @@ fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> an // host_println!("Running NFS mount command: `{}`", &shell_script); // TODO: elevate if needed (e.g. mounting image under /Volumes) - let status = Command::new("sh") + let mut hnd = Command::new("sh") .arg("-c") .arg(&shell_script) - // .stdout(Stdio::null()) - // .stderr(Stdio::null()) - .status()?; // TODO: make sure any error is properly printed + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + utils::echo_child_output(&mut hnd, Some(log::Prefix::Host)); + let status = hnd.wait().context("Failed to wait for NFS mount command")?; if !status.success() { return Err(anyhow!( diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 9192984..556dd85 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1,9 +1,6 @@ use anyhow::{Context, anyhow}; use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; -use common_utils::{ - CustomActionConfig, Deferred, host_eprintln, host_println, log, prefix_eprintln, - prefix_println, safe_println, -}; +use common_utils::{CustomActionConfig, Deferred, host_eprintln, host_println, log, safe_println}; use devinfo::DevInfo; use nanoid::nanoid; @@ -1406,25 +1403,7 @@ fn init_rootfs(config: &Config, force: bool) -> anyhow::Result<()> { .spawn() .context("Failed to execute init-rootfs")?; - let out = BufReader::new(hnd.stdout.take().unwrap()); - let err = BufReader::new(hnd.stderr.take().unwrap()); - - let thread = thread::spawn(move || { - for line in err.lines() { - if let Ok(line) = line { - prefix_println!(None, "{}", line); - } - } - }); - - for line in out.lines() { - if let Ok(line) = line { - prefix_eprintln!(None, "{}", line); - } - } - - thread.join().unwrap(); - + utils::echo_child_output(&mut hnd, None); let status = hnd.wait().context("Failed to wait for init-rootfs")?; if !status.success() { diff --git a/anylinuxfs/src/utils.rs b/anylinuxfs/src/utils.rs index d451db4..c8a32fd 100644 --- a/anylinuxfs/src/utils.rs +++ b/anylinuxfs/src/utils.rs @@ -4,7 +4,7 @@ use std::{ error::Error, ffi::{CString, c_void}, fs::{File, Permissions}, - io::{self, Read, Write}, + io::{self, BufRead, BufReader, Read, Write}, mem::ManuallyDrop, net::IpAddr, os::{ @@ -12,6 +12,7 @@ use std::{ unix::fs::PermissionsExt, }, path::Path, + process::Child, ptr::null, sync::{ Arc, @@ -23,7 +24,11 @@ use std::{ }; use anyhow::{Context, anyhow}; -use common_utils::{host_println, log::Prefix, prefix_print, safe_print}; +use common_utils::{ + host_println, + log::{self, Prefix}, + prefix_eprintln, prefix_print, prefix_println, safe_print, +}; use crossterm::event::{self, Event}; use nix::{ sys::signal::Signal, @@ -902,6 +907,27 @@ pub fn disable_raw_mode() -> anyhow::Result<()> { Ok(()) } +pub fn echo_child_output(hnd: &mut Child, log_prefix: Option) { + let out = BufReader::new(hnd.stdout.take().unwrap()); + let err = BufReader::new(hnd.stderr.take().unwrap()); + + let thread = thread::spawn(move || { + for line in err.lines() { + if let Ok(line) = line { + prefix_println!(log_prefix, "{}", line); + } + } + }); + + for line in out.lines() { + if let Ok(line) = line { + prefix_eprintln!(log_prefix, "{}", line); + } + } + + thread.join().unwrap(); +} + pub struct StdinForwarder { thread_hnd: Cell>>>, close_tx: mpsc::Sender<()>, diff --git a/common-utils/src/log.rs b/common-utils/src/log.rs index e6852c8..2cc094c 100644 --- a/common-utils/src/log.rs +++ b/common-utils/src/log.rs @@ -53,6 +53,7 @@ pub fn print_log_file() { } } +#[derive(Debug, Clone, Copy)] pub enum Prefix { Host, Guest, From ba9c60650eeb145956c4169c341f8901d527188c Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:14:19 +0200 Subject: [PATCH 51/64] make sure the mount error is logged --- anylinuxfs/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 556dd85..89075a0 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2306,6 +2306,7 @@ impl AppRunner { .map(|item| item.as_str()) .filter(|&export_path| export_path != &share_path); + log::enable_console_log(); let mnt_point_base = config .custom_mount_point .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); @@ -2313,6 +2314,7 @@ impl AppRunner { Ok(_) => {} Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), } + log::disable_console_log(); } // drop privileges back to the original user if he used sudo From 3591410b990b305d6f2ea5796c65c2ba27ef158f Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:25:31 +0200 Subject: [PATCH 52/64] is this enough? --- anylinuxfs/src/fsutil.rs | 24 +++++++++++++++--------- anylinuxfs/src/main.rs | 10 +++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 876c803..7d7bfa9 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -146,7 +146,11 @@ mod dirtrie { } } -fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> anyhow::Result<()> { +fn parallel_mount_recursive( + mnt_point_base: PathBuf, + trie: &dirtrie::Node, + elevate: bool, +) -> anyhow::Result<()> { if let Some((rel_path, nfs_path)) = &trie.paths { let shell_script = format!( "mount -t nfs \"localhost:{}\" \"{}\"", @@ -155,10 +159,11 @@ fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> an ); // host_println!("Running NFS mount command: `{}`", &shell_script); - // TODO: elevate if needed (e.g. mounting image under /Volumes) - let mut hnd = Command::new("sh") - .arg("-c") - .arg(&shell_script) + // elevate if needed (e.g. mounting image under /Volumes) + let cmdline = ["sudo", "sh", "-c", &shell_script]; + let cmdline = if elevate { &cmdline[..] } else { &cmdline[1..] }; + let mut hnd = Command::new(cmdline[0]) + .args(&cmdline[1..]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; @@ -180,9 +185,9 @@ fn parallel_mount_recursive(mnt_point_base: PathBuf, trie: &dirtrie::Node) -> an mnt_point_base.join(rel_path).display() ); } - trie.children - .par_iter() - .try_for_each(|(_, child)| parallel_mount_recursive(mnt_point_base.clone(), child))?; + trie.children.par_iter().try_for_each(|(_, child)| { + parallel_mount_recursive(mnt_point_base.clone(), child, elevate) + })?; Ok(()) } @@ -191,6 +196,7 @@ pub fn mount_nfs_subdirs<'a>( share_path_base: &str, subdirs: impl Iterator, mnt_point_base: impl AsRef, + elevate: bool, ) -> anyhow::Result<()> { let mut trie = dirtrie::Node::default(); @@ -202,7 +208,7 @@ pub fn mount_nfs_subdirs<'a>( trie.insert(Path::new(subdir_relative), subdir); } - parallel_mount_recursive(mnt_point_base.as_ref().into(), &trie)?; + parallel_mount_recursive(mnt_point_base.as_ref().into(), &trie, elevate)?; // host_println!("Mounted NFS subdirectories:\r\n{}", trie); Ok(()) } diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 89075a0..bd11761 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2310,7 +2310,15 @@ impl AppRunner { let mnt_point_base = config .custom_mount_point .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); - match mount_nfs_subdirs(&share_path, additional_exports, mnt_point_base) { + + let elevate = + config.common.sudo_uid.is_none() && config.common.invoker_uid != 0; + match mount_nfs_subdirs( + &share_path, + additional_exports, + mnt_point_base, + elevate, + ) { Ok(_) => {} Err(e) => host_eprintln!("Failed to mount additional NFS exports: {:#}", e), } From 3a94e51fc80a33d62993f4ce01a84120dc77d276 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:28:06 +0200 Subject: [PATCH 53/64] fix sudo flags --- anylinuxfs/src/fsutil.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 7d7bfa9..47583ec 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -160,8 +160,8 @@ fn parallel_mount_recursive( // host_println!("Running NFS mount command: `{}`", &shell_script); // elevate if needed (e.g. mounting image under /Volumes) - let cmdline = ["sudo", "sh", "-c", &shell_script]; - let cmdline = if elevate { &cmdline[..] } else { &cmdline[1..] }; + let cmdline = ["sudo", "-S", "sh", "-c", &shell_script]; + let cmdline = if elevate { &cmdline[..] } else { &cmdline[2..] }; let mut hnd = Command::new(cmdline[0]) .args(&cmdline[1..]) .stdout(Stdio::piped()) From d85e443b561fc841622b1f13547d48dbae42f839 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:49:01 +0200 Subject: [PATCH 54/64] sudo is problematic with stdin redirect to VM, try osascript --- anylinuxfs/src/fsutil.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 47583ec..95b63ab 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -160,8 +160,18 @@ fn parallel_mount_recursive( // host_println!("Running NFS mount command: `{}`", &shell_script); // elevate if needed (e.g. mounting image under /Volumes) - let cmdline = ["sudo", "-S", "sh", "-c", &shell_script]; - let cmdline = if elevate { &cmdline[..] } else { &cmdline[2..] }; + let cmdline: &[&str] = if elevate { + &[ + "osascript", + "-e", + &format!( + "do shell script \"{}\" with administrator privileges", + &shell_script + ), + ] + } else { + &["sh", "-c", &shell_script] + }; let mut hnd = Command::new(cmdline[0]) .args(&cmdline[1..]) .stdout(Stdio::piped()) From a9db7d2032ac59c632107d720e1f05595b285af1 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:51:20 +0200 Subject: [PATCH 55/64] try to fix escaping --- anylinuxfs/src/fsutil.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 95b63ab..262ad9b 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -153,7 +153,7 @@ fn parallel_mount_recursive( ) -> anyhow::Result<()> { if let Some((rel_path, nfs_path)) = &trie.paths { let shell_script = format!( - "mount -t nfs \"localhost:{}\" \"{}\"", + "mount -t nfs 'localhost:{}' '{}'", nfs_path, mnt_point_base.join(rel_path).display() ); From cab532ef17816426be92673587433d3d14681eba Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 14:57:31 +0200 Subject: [PATCH 56/64] back to sudo --- anylinuxfs/src/fsutil.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 262ad9b..47583ec 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -153,25 +153,15 @@ fn parallel_mount_recursive( ) -> anyhow::Result<()> { if let Some((rel_path, nfs_path)) = &trie.paths { let shell_script = format!( - "mount -t nfs 'localhost:{}' '{}'", + "mount -t nfs \"localhost:{}\" \"{}\"", nfs_path, mnt_point_base.join(rel_path).display() ); // host_println!("Running NFS mount command: `{}`", &shell_script); // elevate if needed (e.g. mounting image under /Volumes) - let cmdline: &[&str] = if elevate { - &[ - "osascript", - "-e", - &format!( - "do shell script \"{}\" with administrator privileges", - &shell_script - ), - ] - } else { - &["sh", "-c", &shell_script] - }; + let cmdline = ["sudo", "-S", "sh", "-c", &shell_script]; + let cmdline = if elevate { &cmdline[..] } else { &cmdline[2..] }; let mut hnd = Command::new(cmdline[0]) .args(&cmdline[1..]) .stdout(Stdio::piped()) From fdb92222df27eba824c410ab0a48fe8d8c60d668 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 15:13:20 +0200 Subject: [PATCH 57/64] maybe this could fix it? --- anylinuxfs/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index bd11761..1a41a0d 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2292,6 +2292,8 @@ impl AppRunner { None }; + deferred.call_now(disable_stdin_fwd_action); + if let Some(mount_point) = &mount_point_opt { host_println!( "{} was mounted as {}", @@ -2335,8 +2337,6 @@ impl AppRunner { // stop printing to the console log::disable_console_log(); - - deferred.call_now(disable_stdin_fwd_action); } else { // tell the parent to wait for the child to exit unsafe { write_to_pipe(comm_write_fd, b"join\n") } From c2e01c7cf584118a9c88b8144eacc4c6a5db9953 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 15:36:28 +0200 Subject: [PATCH 58/64] maybe we can just do this since we are no longer in raw terminal mode --- anylinuxfs/src/fsutil.rs | 17 ++++------------- anylinuxfs/src/main.rs | 11 ++++++++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/anylinuxfs/src/fsutil.rs b/anylinuxfs/src/fsutil.rs index 47583ec..80206a2 100644 --- a/anylinuxfs/src/fsutil.rs +++ b/anylinuxfs/src/fsutil.rs @@ -1,5 +1,5 @@ -use anyhow::{Context, anyhow}; -use common_utils::{host_println, log}; +use anyhow::anyhow; +use common_utils::host_println; use rayon::prelude::*; use std::{ collections::HashSet, @@ -7,12 +7,10 @@ use std::{ io, mem, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::Command, ptr::null_mut, }; -use crate::utils; - #[derive(Debug, Clone)] pub struct MountTable { disks: HashSet, @@ -162,14 +160,7 @@ fn parallel_mount_recursive( // elevate if needed (e.g. mounting image under /Volumes) let cmdline = ["sudo", "-S", "sh", "-c", &shell_script]; let cmdline = if elevate { &cmdline[..] } else { &cmdline[2..] }; - let mut hnd = Command::new(cmdline[0]) - .args(&cmdline[1..]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - utils::echo_child_output(&mut hnd, Some(log::Prefix::Host)); - let status = hnd.wait().context("Failed to wait for NFS mount command")?; + let status = Command::new(cmdline[0]).args(&cmdline[1..]).status()?; if !status.success() { return Err(anyhow!( diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 1a41a0d..44cbe6c 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2303,10 +2303,11 @@ impl AppRunner { rt_info.lock().unwrap().mount_point = Some(mount_point.display().into()); - let additional_exports = exports + let additional_exports: Vec<_> = exports .iter() .map(|item| item.as_str()) - .filter(|&export_path| export_path != &share_path); + .filter(|&export_path| export_path != &share_path) + .collect(); log::enable_console_log(); let mnt_point_base = config @@ -2315,9 +2316,13 @@ impl AppRunner { let elevate = config.common.sudo_uid.is_none() && config.common.invoker_uid != 0; + + if elevate && !additional_exports.is_empty() { + host_println!("need to use sudo to mount additional NFS exports"); + } match mount_nfs_subdirs( &share_path, - additional_exports, + additional_exports.into_iter(), mnt_point_base, elevate, ) { From bd45e8db5f4c0d56ed1546eea1679d82c1c5b400 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 17:27:29 +0200 Subject: [PATCH 59/64] `anylinuxfs unmount`: accept partial disk_path match --- anylinuxfs/src/main.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 44cbe6c..5e46894 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2440,18 +2440,17 @@ impl AppRunner { }; if let Some(path) = &cmd.path { - match fs::canonicalize(path) { - Ok(abs_path) => { - if abs_path != Path::new(&rt_info.mount_config.disk_path) - && abs_path != mount_point - { - println!("The specified path was not mounted by anylinuxfs."); - return Ok(()); - } - } - Err(err) => { - return Err(err).with_context(|| format!("invalid path {}", path)); - } + let path = fs::canonicalize(path).unwrap_or(PathBuf::from(path)); + if path != Path::new(&rt_info.mount_config.disk_path) + && path != mount_point + && !rt_info + .mount_config + .disk_path + .split(':') + .any(|p| p == path.to_string_lossy()) + { + println!("The specified path was not mounted by anylinuxfs."); + return Ok(()); } } From f3d86e290a2f0f41811c8f250b4986a55814b382 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 17:33:45 +0200 Subject: [PATCH 60/64] NFSv4 will not be used at all --- anylinuxfs/src/main.rs | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 5e46894..b55c88c 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -1227,12 +1227,10 @@ fn wait_for_nfs_server( Ok(nfs_ready) } -fn mount_nfs(share_path: &str, config: &MountConfig, vers4: bool) -> anyhow::Result<()> { +fn mount_nfs(share_path: &str, config: &MountConfig) -> anyhow::Result<()> { let status = if let Some(mount_point) = config.custom_mount_point.as_deref() { - let opts = if vers4 { "-o vers=4" } else { "" }; let mut shell_script = format!( - "mount -t nfs {} \"localhost:{}\" \"{}\"", - opts, + "mount -t nfs \"localhost:{}\" \"{}\"", share_path, mount_point.display() ); @@ -2260,26 +2258,8 @@ impl AppRunner { } _ => format!("/mnt/{share_name}"), }; - // let vers4 = mnt_dev_info.fs_type() == Some("zfs"); - let vers4 = false; - if vers4 { - let mnt_point = PathBuf::from(format!("/Volumes/{share_name}")); - fs::create_dir_all(&mnt_point).with_context(|| { - format!("Failed to create mount point {}", mnt_point.display()) - })?; - - chown( - &mnt_point, - Some(config.common.invoker_uid), - Some(config.common.invoker_gid), - ) - .with_context(|| { - format!("Failed to change owner of {}", mnt_point.display()) - })?; - config.custom_mount_point = Some(mnt_point); - } - let mount_result = mount_nfs(&share_path, &config, vers4); + let mount_result = mount_nfs(&share_path, &config); match &mount_result { Ok(_) => host_println!("Requested NFS share mount"), Err(e) => host_eprintln!("Failed to request NFS mount: {:#}", e), From 2e51d70d0b582b94920260c5bfa0e0e408fffb1d Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 17:58:46 +0200 Subject: [PATCH 61/64] optimization: we don't need to collect additional_exports into a vector --- anylinuxfs/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index b55c88c..b397b77 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2283,11 +2283,11 @@ impl AppRunner { rt_info.lock().unwrap().mount_point = Some(mount_point.display().into()); - let additional_exports: Vec<_> = exports + let mut additional_exports = exports .iter() .map(|item| item.as_str()) .filter(|&export_path| export_path != &share_path) - .collect(); + .peekable(); log::enable_console_log(); let mnt_point_base = config @@ -2297,7 +2297,7 @@ impl AppRunner { let elevate = config.common.sudo_uid.is_none() && config.common.invoker_uid != 0; - if elevate && !additional_exports.is_empty() { + if elevate && additional_exports.peek().is_some() { host_println!("need to use sudo to mount additional NFS exports"); } match mount_nfs_subdirs( From 6681c8717764452b1911c6b5ddb76aa07eae60d4 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 22:08:41 +0200 Subject: [PATCH 62/64] added global config.toml in etc which will be used for pre-defined custom actions --- anylinuxfs/src/main.rs | 314 ++++++++++++++++++++++++++++--------- common-utils/src/lib.rs | 2 + etc/anylinuxfs/config.toml | 8 + install.sh | 2 + 4 files changed, 250 insertions(+), 76 deletions(-) create mode 100644 etc/anylinuxfs/config.toml diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index b397b77..8994e18 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -117,11 +117,28 @@ struct Config { sudo_uid: Option, sudo_gid: Option, passphrase_config: PassphrasePromptConfig, - preferences: Preferences, + preferences: [PrefsObject; 2], +} + +trait Preferences { + fn alpine_custom_packages<'a>(&'a self) -> BTreeSet<&'a str>; + fn custom_actions<'a>(&'a self) -> HashMap<&'a str, &'a CustomActionConfig>; + fn gvproxy_debug(&self) -> bool; + fn krun_log_level_numeric(&self) -> u32; + fn krun_num_vcpus(&self) -> u8; + fn krun_ram_size_mib(&self) -> u32; + fn passphrase_prompt_config(&self) -> PassphrasePromptConfig; + + fn user<'a>(&'a self) -> &'a PrefsObject; + fn user_mut<'a>(&'a mut self) -> &'a mut PrefsObject; + // fn global<'a>(&'a self) -> &'a PrefsObject; + // fn global_mut<'a>(&'a mut self) -> &'a mut PrefsObject; + + fn merged(&self) -> PrefsObject; } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -struct Preferences { +struct PrefsObject { #[serde(default)] alpine: AlpineConfig, #[serde(default)] @@ -139,7 +156,99 @@ struct Preferences { ram_size_mib: Option, } -impl Display for Preferences { +impl Preferences for [PrefsObject; 2] { + fn alpine_custom_packages<'a>(&'a self) -> BTreeSet<&'a str> { + let mut result = + BTreeSet::from_iter(self[0].alpine.custom_packages.iter().map(|s| s.as_str())); + result.extend(self[1].alpine.custom_packages.iter().map(|s| s.as_str())); + result + } + + fn custom_actions<'a>(&'a self) -> HashMap<&'a str, &'a CustomActionConfig> { + let mut result: HashMap<_, _> = self[0] + .custom_actions + .iter() + .map(|(k, v)| (k.as_str(), v)) + .collect(); + result.extend(self[1].custom_actions.iter().map(|(k, v)| (k.as_str(), v))); + result + } + + fn gvproxy_debug(&self) -> bool { + self[1] + .gvproxy + .debug + .or(self[0].gvproxy.debug) + .unwrap_or(false) + } + + fn krun_log_level_numeric(&self) -> u32 { + self[1] + .krun + .log_level_numeric + .or(self[0].krun.log_level_numeric) + .unwrap_or(KrunConfig::default_log_level()) + } + + fn krun_num_vcpus(&self) -> u8 { + self[1] + .krun + .num_vcpus + .or(self[0].krun.num_vcpus) + .unwrap_or(KrunConfig::default_num_vcpus()) + } + + fn krun_ram_size_mib(&self) -> u32 { + self[1] + .krun + .ram_size_mib + .or(self[0].krun.ram_size_mib) + .unwrap_or(KrunConfig::default_ram_size()) + } + + fn passphrase_prompt_config(&self) -> PassphrasePromptConfig { + self[1].misc.passphrase_config + } + + fn user<'a>(&'a self) -> &'a PrefsObject { + &self[1] + } + + fn user_mut<'a>(&'a mut self) -> &'a mut PrefsObject { + &mut self[1] + } + + // fn global<'a>(&'a self) -> &'a PrefsObject { + // &self[0] + // } + + // fn global_mut<'a>(&'a mut self) -> &'a mut PrefsObject { + // &mut self[0] + // } + + fn merged(&self) -> PrefsObject { + let result = self[0].clone(); + result.merge_with(&self[1]) + } +} + +impl PrefsObject { + fn merge_with(&self, other: &PrefsObject) -> PrefsObject { + let mut custom_actions = self.custom_actions.clone(); + custom_actions.extend(other.custom_actions.clone()); + + PrefsObject { + alpine: self.alpine.merge_with(&other.alpine), + custom_actions, + gvproxy: self.gvproxy.merge_with(&other.gvproxy), + krun: self.krun.merge_with(&other.krun), + misc: self.misc.merge_with(&other.misc), + ..Default::default() + } + } +} + +impl Display for PrefsObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[krun]\n{}", self.krun)?; write!(f, "\n\n[misc]\n{}", self.misc)?; @@ -152,6 +261,16 @@ struct AlpineConfig { custom_packages: Vec, } +impl AlpineConfig { + fn merge_with(&self, other: &AlpineConfig) -> AlpineConfig { + let mut custom_packages = BTreeSet::from_iter(self.custom_packages.clone()); + custom_packages.extend(other.custom_packages.clone()); + AlpineConfig { + custom_packages: custom_packages.into_iter().collect(), + } + } +} + trait CustomActionEnvironment { fn prepare_environment(&self) -> anyhow::Result>; } @@ -209,17 +328,23 @@ impl CustomActionEnvironment for CustomActionConfig { #[derive(Clone, Debug, Default, Deserialize, Serialize)] struct GvproxyConfig { - debug: bool, + debug: Option, } -#[derive(Clone, Debug, Deserialize, Serialize)] +impl GvproxyConfig { + fn merge_with(&self, other: &GvproxyConfig) -> GvproxyConfig { + GvproxyConfig { + debug: other.debug.or(self.debug), + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] struct KrunConfig { - #[serde(default = "KrunConfig::default_log_level", rename = "log_level")] - log_level_numeric: u32, - #[serde(default = "KrunConfig::default_num_vcpus")] - num_vcpus: u8, - #[serde(default = "KrunConfig::default_ram_size")] - ram_size_mib: u32, + #[serde(rename = "log_level")] + log_level_numeric: Option, + num_vcpus: Option, + ram_size_mib: Option, } impl KrunConfig { @@ -234,14 +359,12 @@ impl KrunConfig { fn default_ram_size() -> u32 { 512 } -} -impl Default for KrunConfig { - fn default() -> Self { + fn merge_with(&self, other: &KrunConfig) -> KrunConfig { KrunConfig { - log_level_numeric: 0, - num_vcpus: 1, - ram_size_mib: 512, + log_level_numeric: other.log_level_numeric.or(self.log_level_numeric), + num_vcpus: other.num_vcpus.or(self.num_vcpus), + ram_size_mib: other.ram_size_mib.or(self.ram_size_mib), } } } @@ -252,8 +375,8 @@ impl Display for KrunConfig { f, "log_level = {}\nnum_vcpus = {}\nram_size_mib = {}", self.log_level(), - self.num_vcpus, - self.ram_size_mib + self.num_vcpus.unwrap_or(KrunConfig::default_num_vcpus()), + self.ram_size_mib.unwrap_or(KrunConfig::default_ram_size()) ) } } @@ -299,11 +422,13 @@ impl From for KrunLogLevel { #[allow(unused)] impl KrunConfig { fn log_level(&self) -> KrunLogLevel { - self.log_level_numeric.into() + self.log_level_numeric + .unwrap_or(KrunConfig::default_log_level()) + .into() } fn set_log_level(&mut self, level: KrunLogLevel) { - self.log_level_numeric = level as u32; + self.log_level_numeric = Some(level as u32); } } @@ -312,6 +437,12 @@ struct MiscConfig { passphrase_config: PassphrasePromptConfig, } +impl MiscConfig { + fn merge_with(&self, other: &MiscConfig) -> MiscConfig { + other.clone() + } +} + impl Display for MiscConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "passphrase_config = {}", self.passphrase_config) @@ -335,8 +466,13 @@ struct MountConfig { impl MountConfig { fn get_action(&self) -> Option<&CustomActionConfig> { - match &self.custom_action { - Some(action_name) => self.common.preferences.custom_actions.get(action_name), + match self.custom_action.as_deref() { + Some(action_name) => self + .common + .preferences + .custom_actions() + .get(action_name) + .map(|a| *a), None => None, } } @@ -420,6 +556,8 @@ Supported partition schemes: after_help = "Lists all physical partitions and LVM/RAID volumes. Can decrypt LUKS partition metadata too." )] List(ListCmd), + /// List available custom actions + Actions, /// Stop anylinuxfs (can be used if unresponsive) Stop(StopCmd), /// microVM shell for debugging (configures the VM according to mount options but only starts a shell) @@ -675,12 +813,17 @@ fn load_config(common_args: &CommonArgs) -> anyhow::Result { let vsock_path = format!("/tmp/anylinuxfs-{}-vsock", rand_string(8)); let vfkit_sock_path = format!("/tmp/vfkit-{}.sock", rand_string(8)); - let preferences = load_preferences(&config_file_path)?; + let global_cfg_path = prefix_dir + .join("etc") + .join("anylinuxfs") + .join("config.toml"); + let all_cfg_paths = [global_cfg_path.as_path(), config_file_path.as_path()]; + let preferences = load_preferences(all_cfg_paths.iter().cloned())?; // println!("Loaded preferences: {:#?}", &preferences); let passphrase_config = common_args .passphrase_config - .unwrap_or(preferences.misc.passphrase_config); + .unwrap_or(preferences.passphrase_prompt_config()); Ok(Config { exec_path, @@ -703,33 +846,44 @@ fn load_config(common_args: &CommonArgs) -> anyhow::Result { }) } -fn convert_legacy_config(config: &mut Preferences) { +fn convert_legacy_config(config: &mut PrefsObject) { if let Some(log_level_numeric) = config.log_level_numeric.take() { - config.krun.log_level_numeric = log_level_numeric; + config.krun.log_level_numeric = Some(log_level_numeric); } if let Some(num_vcpus) = config.num_vcpus.take() { - config.krun.num_vcpus = num_vcpus; + config.krun.num_vcpus = Some(num_vcpus); } if let Some(ram_size_mib) = config.ram_size_mib.take() { - config.krun.ram_size_mib = ram_size_mib; + config.krun.ram_size_mib = Some(ram_size_mib); } } -fn load_preferences(path: &Path) -> anyhow::Result { - match fs::read_to_string(path) { - Ok(config_str) => { - let mut config: Preferences = toml::from_str(&config_str) - .context(format!("Failed to parse config file {}", path.display()))?; - convert_legacy_config(&mut config); - Ok(config) - } - Err(_) => Ok(Preferences::default()), +fn load_preferences<'a>(paths: impl Iterator) -> anyhow::Result<[PrefsObject; 2]> { + let mut result_config = [PrefsObject::default(), PrefsObject::default()]; + let mut cfg_idx = 0; + for path in paths { + match fs::read_to_string(path) { + Ok(config_str) => { + let mut config = toml::from_str(&config_str) + .context(format!("Failed to parse config file {}", path.display()))?; + convert_legacy_config(&mut config); + result_config[cfg_idx] = config; + } + Err(_) => (), + }; + cfg_idx += 1; } + Ok(result_config) } -fn save_preferences(preferences: &Preferences, config_file_path: &Path) -> anyhow::Result<()> { +fn save_preferences(preferences: &PrefsObject, config_file_path: &Path) -> anyhow::Result<()> { let config_str = toml::to_string(preferences).context("Failed to serialize Preferences to TOML")?; + // println!( + // "Saving config to {}:\n{}", + // config_file_path.display(), + // config_str + // ); fs::write(config_file_path, config_str).context(format!( "Failed to write config file {}", config_file_path.display() @@ -788,9 +942,9 @@ fn load_mount_config(cmd: MountCmd) -> anyhow::Result { let open_finder = cmd.window; - let custom_action = if let Some(action_name) = cmd.action { - match common.preferences.custom_actions.get(&action_name) { - Some(_) => Some(action_name), + let custom_action = if let Some(action_name) = cmd.action.as_deref() { + match common.preferences.custom_actions().get(&action_name) { + Some(_) => Some(action_name.to_owned()), None => { return Err(anyhow::anyhow!("unknown custom action: {}", action_name)); } @@ -838,11 +992,11 @@ fn setup_vm( ) -> anyhow::Result { let ctx = unsafe { bindings::krun_create_ctx() }.context("Failed to create context")?; - let level = config.preferences.krun.log_level_numeric; + let level = config.preferences.krun_log_level_numeric(); unsafe { bindings::krun_set_log_level(level) }.context("Failed to set log level")?; - let num_vcpus = config.preferences.krun.num_vcpus; - let ram_mib = config.preferences.krun.ram_size_mib; + let num_vcpus = config.preferences.krun_num_vcpus(); + let ram_mib = config.preferences.krun_ram_size_mib(); unsafe { bindings::krun_set_vm_config(ctx, num_vcpus, ram_mib) } .context("Failed to set VM config")?; @@ -1155,7 +1309,7 @@ fn start_gvproxy(config: &Config) -> anyhow::Result { "-1", ]; - if config.preferences.gvproxy.debug { + if config.preferences.gvproxy_debug() { gvproxy_args.push("--debug"); } @@ -1636,11 +1790,11 @@ fn claim_devices(config: &MountConfig) -> anyhow::Result<(Vec, DevInfo, } fn ensure_enough_ram_for_luks(config: &mut Config) { - if config.preferences.krun.ram_size_mib < 2560 { - config.preferences.krun.ram_size_mib = 2560; + if config.preferences.krun_ram_size_mib() < 2560 { + config.preferences.user_mut().krun.ram_size_mib = Some(2560); println!( "Configured RAM size is lower than the minimum required for LUKS decryption, setting to {} MiB", - config.preferences.krun.ram_size_mib + config.preferences.krun_ram_size_mib() ); } } @@ -1692,10 +1846,10 @@ impl AppRunner { // host_println!("disk_path: {}", config.disk_path); host_println!("root_path: {}", config.common.root_path.display()); - host_println!("num_vcpus: {}", config.common.preferences.krun.num_vcpus); + host_println!("num_vcpus: {}", config.common.preferences.krun_num_vcpus()); host_println!( "ram_size_mib: {}", - config.common.preferences.krun.ram_size_mib + config.common.preferences.krun_ram_size_mib() ); let (dev_info, _, _disks) = claim_devices(&config)?; @@ -1741,7 +1895,7 @@ impl AppRunner { let mut config = load_config(&CommonArgs::default())?; let config_file_path = &config.config_file_path; - let alpine_config = &mut config.preferences.alpine; + let alpine_packages = config.preferences.alpine_custom_packages(); let default_packages = get_default_packages(); let dns_server = dnsutil::get_dns_server_with_fallback(); @@ -1749,7 +1903,7 @@ impl AppRunner { let apk_command = match cmd { ApkCmd::Info => { // Show information about custom packages - for pkg in &alpine_config.custom_packages { + for pkg in alpine_packages { safe_println!("{}", pkg)?; } return Ok(()); @@ -1759,10 +1913,10 @@ impl AppRunner { packages.retain(|pkg| !default_packages.contains(pkg)); // Add custom packages - let mut package_set: BTreeSet<_> = - BTreeSet::from_iter(alpine_config.custom_packages.iter().cloned()); - package_set.extend(packages.iter().cloned()); - alpine_config.custom_packages = package_set.into_iter().collect(); + let mut package_set = alpine_packages.clone(); + package_set.extend(packages.iter().map(|s| s.as_str())); + config.preferences.user_mut().alpine.custom_packages = + package_set.into_iter().map(|s| s.to_owned()).collect(); if packages.is_empty() { // no-op @@ -1776,7 +1930,10 @@ impl AppRunner { packages.retain(|pkg| !default_packages.contains(pkg)); // Remove custom packages - alpine_config + config + .preferences + .user_mut() + .alpine .custom_packages .retain(|pkg| !packages.contains(pkg)); @@ -1801,7 +1958,7 @@ impl AppRunner { )); } // preferences are only saved if apk command was successful - save_preferences(&config.preferences, config_file_path)?; + save_preferences(config.preferences.user(), config_file_path)?; Ok(()) } @@ -1895,10 +2052,10 @@ impl AppRunner { // host_println!("disk_path: {}", config.disk_path); host_println!("root_path: {}", config.common.root_path.display()); - host_println!("num_vcpus: {}", config.common.preferences.krun.num_vcpus); + host_println!("num_vcpus: {}", config.common.preferences.krun_num_vcpus()); host_println!( "ram_size_mib: {}", - config.common.preferences.krun.ram_size_mib + config.common.preferences.krun_ram_size_mib() ); let (dev_info, mut mnt_dev_info, _disks) = claim_devices(&config)?; @@ -2473,32 +2630,28 @@ impl AppRunner { let mut config = load_config(&cmd.common)?; let config_file_path = &config.config_file_path; - let krun_config = &mut config.preferences.krun; + let krun_config = &mut config.preferences.user_mut().krun; if cmd == ConfigCmd::default() { - println!("{}", &config.preferences); + println!("{}", &config.preferences.merged()); return Ok(()); } if let Some(log_level) = cmd.log_level { krun_config.set_log_level(log_level); } - if let Some(num_vcpus) = cmd.num_vcpus { - krun_config.num_vcpus = num_vcpus; - } - if let Some(ram_size_mib) = cmd.ram_size_mib { - krun_config.ram_size_mib = ram_size_mib; - } - let misc_config = &mut config.preferences.misc; + krun_config.num_vcpus = cmd.num_vcpus; + krun_config.ram_size_mib = cmd.ram_size_mib; - if let Some(pwd_cfg) = cmd.common.passphrase_config { - misc_config.passphrase_config = pwd_cfg; + let misc_config = &mut config.preferences.user_mut().misc; + if let Some(passphrase_config) = cmd.common.passphrase_config { + misc_config.passphrase_config = passphrase_config; } - println!("{}", &config.preferences); + println!("{}", &config.preferences.merged()); - save_preferences(&config.preferences, config_file_path)?; + save_preferences(config.preferences.user(), config_file_path)?; Ok(()) } @@ -2522,6 +2675,14 @@ impl AppRunner { Ok(()) } + fn run_actions(&mut self) -> anyhow::Result<()> { + let config = load_config(&CommonArgs::default())?; + for (action, config) in config.preferences.custom_actions() { + safe_println!("{}: {}", action, &config.description)?; + } + Ok(()) + } + fn run_log(&mut self, cmd: LogCmd) -> anyhow::Result<()> { let config = load_config(&CommonArgs::default())?; let log_file_path = &config.log_file_path; @@ -2620,8 +2781,8 @@ impl AppRunner { mount_point.display(), info.join(", "), &user_name, - rt_info.mount_config.common.preferences.krun.num_vcpus, - rt_info.mount_config.common.preferences.krun.ram_size_mib, + rt_info.mount_config.common.preferences.krun_num_vcpus(), + rt_info.mount_config.common.preferences.krun_ram_size_mib(), ); } Err(err) => { @@ -2749,6 +2910,7 @@ impl AppRunner { Commands::Log(cmd) => self.run_log(cmd), Commands::Config(cmd) => self.run_config(cmd), Commands::List(cmd) => self.run_list(cmd), + Commands::Actions => self.run_actions(), Commands::Stop(cmd) => self.run_stop(cmd), Commands::Shell(cmd) => self.run_shell(cmd), Commands::Dmesg => self.run_dmesg(), diff --git a/common-utils/src/lib.rs b/common-utils/src/lib.rs index a10296e..5598edd 100644 --- a/common-utils/src/lib.rs +++ b/common-utils/src/lib.rs @@ -115,6 +115,8 @@ impl<'a> Drop for Deferred<'a> { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CustomActionConfig { + #[serde(default)] + pub description: String, #[serde(default)] pub before_mount: String, #[serde(default)] diff --git a/etc/anylinuxfs/config.toml b/etc/anylinuxfs/config.toml new file mode 100644 index 0000000..83e5e3c --- /dev/null +++ b/etc/anylinuxfs/config.toml @@ -0,0 +1,8 @@ +[custom_actions.ubuntu_zfs_unlock] +description = "Mount ZFS keystore encrypted with LUKS to unlock Ubuntu system disk" +before_mount = "cryptsetup open /dev/zd0 ks0 && mkdir -p $KS && mount /dev/mapper/ks0 $KS" +after_mount = "" +before_unmount = "umount /dev/mapper/ks0 && cryptsetup close ks0" +environment = ["KS=/run/keystore/rpool"] +capture_environment = [] +override_nfs_export = "" diff --git a/install.sh b/install.sh index b2296d7..73430c5 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,7 @@ PREFIX=${1:-"/opt/anylinuxfs"} # sudo mkdir -p "$PREFIX" mkdir -p "$PREFIX/bin" +mkdir -p "$PREFIX/etc/anylinuxfs" mkdir -p "$PREFIX/libexec" # sudo chown $(whoami):admin "$PREFIX" @@ -21,6 +22,7 @@ function fix_libkrun_path() { # fix_libkrun_path "$PREFIX/bin/anylinuxfs" # codesign --entitlements "anylinuxfs.entitlements" --force -s - "$PREFIX/bin/anylinuxfs" +cp -RL etc/* "$PREFIX/etc/" cp -RL libexec/* "$PREFIX/libexec/" # not needed when using libkrun from homebrew From ab849b69c607fb78ed874688214c11b1534400a1 Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sat, 18 Oct 2025 22:22:03 +0200 Subject: [PATCH 63/64] fix bug in preferences merging --- anylinuxfs/src/main.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 8994e18..2a72bab 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -207,7 +207,11 @@ impl Preferences for [PrefsObject; 2] { } fn passphrase_prompt_config(&self) -> PassphrasePromptConfig { - self[1].misc.passphrase_config + self[1] + .misc + .passphrase_config + .or(self[0].misc.passphrase_config) + .unwrap_or_default() } fn user<'a>(&'a self) -> &'a PrefsObject { @@ -434,18 +438,24 @@ impl KrunConfig { #[derive(Clone, Debug, Default, Deserialize, Serialize)] struct MiscConfig { - passphrase_config: PassphrasePromptConfig, + passphrase_config: Option, } impl MiscConfig { fn merge_with(&self, other: &MiscConfig) -> MiscConfig { - other.clone() + MiscConfig { + passphrase_config: other.passphrase_config.or(self.passphrase_config.clone()), + } + } + + fn passphrase_config(&self) -> PassphrasePromptConfig { + self.passphrase_config.unwrap_or_default() } } impl Display for MiscConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "passphrase_config = {}", self.passphrase_config) + write!(f, "passphrase_config = {}", self.passphrase_config()) } } @@ -2641,12 +2651,16 @@ impl AppRunner { krun_config.set_log_level(log_level); } - krun_config.num_vcpus = cmd.num_vcpus; - krun_config.ram_size_mib = cmd.ram_size_mib; + if let Some(num_vcpus) = cmd.num_vcpus { + krun_config.num_vcpus = Some(num_vcpus); + } + if let Some(ram_size_mib) = cmd.ram_size_mib { + krun_config.ram_size_mib = Some(ram_size_mib); + } let misc_config = &mut config.preferences.user_mut().misc; if let Some(passphrase_config) = cmd.common.passphrase_config { - misc_config.passphrase_config = passphrase_config; + misc_config.passphrase_config = Some(passphrase_config); } println!("{}", &config.preferences.merged()); From 0ce338283a11b1c9bccff1c0f1850126c12ff24d Mon Sep 17 00:00:00 2001 From: Jan Noha Date: Sun, 19 Oct 2025 09:56:33 +0200 Subject: [PATCH 64/64] use the real mount point from disk arbitration as mnt_point_base for mounting additional exports --- anylinuxfs/src/main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/anylinuxfs/src/main.rs b/anylinuxfs/src/main.rs index 2a72bab..0ddf161 100644 --- a/anylinuxfs/src/main.rs +++ b/anylinuxfs/src/main.rs @@ -2457,10 +2457,6 @@ impl AppRunner { .peekable(); log::enable_console_log(); - let mnt_point_base = config - .custom_mount_point - .unwrap_or(PathBuf::from(format!("/Volumes/{share_name}"))); - let elevate = config.common.sudo_uid.is_none() && config.common.invoker_uid != 0; @@ -2470,7 +2466,7 @@ impl AppRunner { match mount_nfs_subdirs( &share_path, additional_exports.into_iter(), - mnt_point_base, + mount_point.display(), elevate, ) { Ok(_) => {}