Add support for sharing host directories into the VM via QEMU 9p (virtio-9p). The --mount flag can be repeated; each host directory appears at the same absolute path inside the VM. Design: - QEMU: each --mount gets a short 9p tag (slim0, slim1, …) via -virtfs local,path=…,mount_tag=slimN,security_model=mapped-xattr. Tags are kept short because 9p mount_tag has a ~31-byte limit. - Kernel cmdline: the full destination path is passed as slim.mount=<tag>:<base64(path)> so the init script knows where to mount each tag. Base64 avoids issues with spaces/special chars. - slim-init.sh: after networking, parse slim.mount= entries, mkdir -p the destination, and mount -t 9p <tag> <dest> -o trans=virtio,version=9p2000.L Tests verify: 9p share detection via sysfs mount_tag, 9p entry in mount output, file content accessible at the expected path, and clean VM exit.
This commit is contained in:
@@ -103,6 +103,55 @@ EOF
|
|||||||
rm -rf "$work"
|
rm -rf "$work"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_mount() {
|
||||||
|
echo "=== Testing --mount (9p shares) ==="
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
img="slim-test-mount"
|
||||||
|
share_dir="$work/share"
|
||||||
|
|
||||||
|
mkdir -p "$share_dir"
|
||||||
|
echo "hello from host" > "$share_dir/testfile.txt"
|
||||||
|
|
||||||
|
cat > "$work/Containerfile" <<'EOF'
|
||||||
|
FROM alpine:latest
|
||||||
|
CMD ["/bin/sh", "-c", "poweroff -f"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "-- Building container image..."
|
||||||
|
if ! podman build --network=none -t "$img" -f "$work/Containerfile" >/dev/null 2>&1; then
|
||||||
|
report fail "--mount (podman build failed)"
|
||||||
|
rm -rf "$work"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "-- Building slim VM..."
|
||||||
|
if ! "$SLIM_BIN" build qcow2 "$img" >/dev/null 2>&1; then
|
||||||
|
report fail "--mount (slim build failed)"
|
||||||
|
cleanup "$img" "$img"
|
||||||
|
rm -rf "$work"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The 9p share appears at the same path inside the VM as on the host.
|
||||||
|
# Use --cmd to run the verification snippet with the canonical path.
|
||||||
|
share_canon=$(readlink -f "$share_dir")
|
||||||
|
|
||||||
|
echo "-- Booting VM with --mount $share_dir ..."
|
||||||
|
output=$(timeout "$TIMEOUT" "$SLIM_BIN" run "$img" \
|
||||||
|
--mount "$share_dir" \
|
||||||
|
--cmd "echo Available 9p shares:; for f in /sys/bus/virtio/drivers/9pnet_virtio/virtio*/mount_tag; do [ -f \"\$f\" ] && echo \"\$f: \$(tr -d '\\0' < \"\$f\")\"; done; echo Mounts:; mount -v; cat \"${share_canon}/testfile.txt\" 2>/dev/null || echo NO_FILE; echo MOUNT_VERIFY_DONE; poweroff -f" \
|
||||||
|
2>&1 || true)
|
||||||
|
|
||||||
|
check_output "$output" "mount_tag: slim0" "--mount 9p share detected"
|
||||||
|
check_output "$output" "type 9p" "--mount 9p filesystem in mount list"
|
||||||
|
check_output "$output" "hello from host" "--mount file accessible in VM"
|
||||||
|
check_output "$output" "MOUNT_VERIFY_DONE" "--mount VM ran to completion"
|
||||||
|
|
||||||
|
cleanup "$img" "$img"
|
||||||
|
rm -rf "$work"
|
||||||
|
}
|
||||||
|
|
||||||
test_service() {
|
test_service() {
|
||||||
echo "=== Testing nextcloud service ==="
|
echo "=== Testing nextcloud service ==="
|
||||||
|
|
||||||
@@ -171,6 +220,8 @@ cargo build 2>&1
|
|||||||
test_distro "alpine" "alpine:latest" ""
|
test_distro "alpine" "alpine:latest" ""
|
||||||
test_distro "archlinux" "archlinux:latest" "RUN pacman -Sy --noconfirm iproute2 wget; pacman -Sc --noconfirm"
|
test_distro "archlinux" "archlinux:latest" "RUN pacman -Sy --noconfirm iproute2 wget; pacman -Sc --noconfirm"
|
||||||
|
|
||||||
|
test_mount
|
||||||
|
|
||||||
test_service
|
test_service
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
+29
-1
@@ -8,7 +8,7 @@
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::{fmt::Write as _, process::Command};
|
use std::{fmt::Write as _, path::PathBuf, process::Command};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
forward::PortForward,
|
forward::PortForward,
|
||||||
@@ -32,6 +32,11 @@ pub struct RunCmd {
|
|||||||
/// cmdline as slim.cmd=<b64>). Overrides the CMD inferred at build time.
|
/// cmdline as slim.cmd=<b64>). Overrides the CMD inferred at build time.
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
cmd: Option<String>,
|
cmd: Option<String>,
|
||||||
|
|
||||||
|
/// Share a host directory into the VM via 9p. The directory appears at
|
||||||
|
/// the same path inside the VM. Can be repeated for multiple mounts.
|
||||||
|
#[clap(long)]
|
||||||
|
mount: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(
|
pub(crate) fn run(
|
||||||
@@ -40,6 +45,7 @@ pub(crate) fn run(
|
|||||||
memory,
|
memory,
|
||||||
forward,
|
forward,
|
||||||
cmd,
|
cmd,
|
||||||
|
mount,
|
||||||
}: RunCmd,
|
}: RunCmd,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let reg_dir = registry_dir(&name)?;
|
let reg_dir = registry_dir(&name)?;
|
||||||
@@ -81,6 +87,27 @@ pub(crate) fn run(
|
|||||||
cmdline.push(format!("slim.cmd={encoded}"));
|
cmdline.push(format!("slim.cmd={encoded}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build 9p shares for each --mount. Tags are short (slim0, slim1, …)
|
||||||
|
// because 9p mount_tag has a ~31-byte limit. The full destination path
|
||||||
|
// is passed on the kernel cmdline as slim.mount=<tag>:<base64(path)>.
|
||||||
|
let mut virtfs_args: Vec<String> = Vec::new();
|
||||||
|
for (i, host_path) in mount.iter().enumerate() {
|
||||||
|
let canonical = host_path
|
||||||
|
.canonicalize()
|
||||||
|
.with_context(|| format!("Cannot resolve mount path '{}'", host_path.display()))?;
|
||||||
|
let path_str = canonical
|
||||||
|
.to_str()
|
||||||
|
.context("Mount path is not valid UTF-8")?;
|
||||||
|
let tag = format!("slim{i}");
|
||||||
|
let dest_b64 = base64::engine::general_purpose::STANDARD.encode(path_str.as_bytes());
|
||||||
|
|
||||||
|
virtfs_args.push("-virtfs".into());
|
||||||
|
virtfs_args.push(format!(
|
||||||
|
"local,path={path_str},mount_tag={tag},security_model=mapped-xattr"
|
||||||
|
));
|
||||||
|
cmdline.push(format!("slim.mount={tag}:{dest_b64}"));
|
||||||
|
}
|
||||||
|
|
||||||
println!("Booting {name} from registry: {}", reg_dir.display());
|
println!("Booting {name} from registry: {}", reg_dir.display());
|
||||||
|
|
||||||
let cmdline = cmdline.join(" ");
|
let cmdline = cmdline.join(" ");
|
||||||
@@ -101,6 +128,7 @@ pub(crate) fn run(
|
|||||||
.arg("-kernel")
|
.arg("-kernel")
|
||||||
.arg(vmlinuz_path.as_os_str())
|
.arg(vmlinuz_path.as_os_str())
|
||||||
.args(&fs_args)
|
.args(&fs_args)
|
||||||
|
.args(&virtfs_args)
|
||||||
.args(["-snapshot"])
|
.args(["-snapshot"])
|
||||||
.args(["-no-reboot"])
|
.args(["-no-reboot"])
|
||||||
.args(["-append", &cmdline])
|
.args(["-append", &cmdline])
|
||||||
|
|||||||
@@ -34,6 +34,30 @@ ip addr add 10.0.2.15/24 dev eth0 2>/dev/null || echo "ip addr add eth0 failed"
|
|||||||
ip route add default via 10.0.2.2 2>/dev/null || echo "ip route add default failed"
|
ip route add default via 10.0.2.2 2>/dev/null || echo "ip route add default failed"
|
||||||
echo nameserver 10.0.2.3 > /etc/resolv.conf
|
echo nameserver 10.0.2.3 > /etc/resolv.conf
|
||||||
|
|
||||||
|
# === 9p shares (host directories shared via --mount) ===
|
||||||
|
# Each share is passed as slim.mount=<tag>:<base64(dest_path)> on the
|
||||||
|
# kernel cmdline. The 9p tag is short (e.g. slim0) because mount_tag
|
||||||
|
# has a ~31-byte limit; the full destination path is base64-encoded.
|
||||||
|
# shellcheck disable=SC2013 # word-splitting is intentional: cmdline tokens are space-separated
|
||||||
|
for tok in $(cat /proc/cmdline 2>/dev/null); do
|
||||||
|
case "$tok" in
|
||||||
|
slim.mount=*)
|
||||||
|
v=${tok#slim.mount=}
|
||||||
|
tag=${v%%:*}
|
||||||
|
dest_b64=${v#*:}
|
||||||
|
dest=$(printf '%s' "$dest_b64" | base64 -d 2>/dev/null) \
|
||||||
|
|| dest=$(printf '%s' "$dest_b64" | openssl base64 -d 2>/dev/null) \
|
||||||
|
|| echo "base64 decode failed"
|
||||||
|
if [ -n "$tag" ] && [ -n "$dest" ]; then
|
||||||
|
mkdir -p "$dest"
|
||||||
|
mount -t 9p "$tag" "$dest" -o trans=virtio,version=9p2000.L 2>/dev/null \
|
||||||
|
&& echo "mounted 9p '$tag' -> '$dest'" \
|
||||||
|
|| echo "failed to mount 9p '$tag' -> '$dest'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# === Execute the configured command ===
|
# === Execute the configured command ===
|
||||||
# If slim.cmd=<base64> is on the kernel cmdline, decode and exec it.
|
# If slim.cmd=<base64> is on the kernel cmdline, decode and exec it.
|
||||||
# Otherwise, exec /slim/exec (generated from the image's CMD/ENTRYPOINT).
|
# Otherwise, exec /slim/exec (generated from the image's CMD/ENTRYPOINT).
|
||||||
|
|||||||
Reference in New Issue
Block a user