1 parent
bfda3e626a
commit
8ad6754328
6 files changed
+301
-47
No files matched your search
+53
-25
@@ -3,7 +3,11 @@
|
||||
//! at $XDG_DATA_HOME/boco/registry/vmlinuz. A universal /boco/init script (injected
|
||||
//! at build time) handles VM bootstrap; /boco/exec runs the container's
|
||||
//! CMD/ENTRYPOINT. Runtime overrides are delivered as `boco.cmd=<base64>`
|
||||
//! on the kernel cmdline.
|
||||
//! on the kernel cmdline. Host directories are shared into the guest via
|
||||
//! virtio-fs: one virtiofsd daemon (under `podman unshare` for rootless
|
||||
//! UID mapping) per `--mount`, plus a `-chardev socket` + `-device
|
||||
//! vhost-user-fs-pci` pair on the QEMU side. The guest mounts each share
|
||||
//! with `mount -t virtiofs <tag> <dest>` (see `boco.mount=` cmdline).
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64::Engine;
|
||||
@@ -13,6 +17,7 @@ use std::{fmt::Write as _, process::Command};
|
||||
use crate::{
|
||||
forward::PortForward,
|
||||
registry::{registry_base_dir, registry_dir},
|
||||
virtiofs::{self, VirtiofsShare},
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -29,11 +34,11 @@ pub struct BootCmd {
|
||||
forward: Vec<PortForward>,
|
||||
|
||||
/// Override the command to exec in the VM (base64-encoded on the kernel
|
||||
/// cmdline as boco.cmd=<b64>). Overrides the CMD inferred at build time.
|
||||
/// cmdline as `boco.cmd=<b64>`). Overrides the CMD inferred at build time.
|
||||
#[clap(long)]
|
||||
cmd: Option<String>,
|
||||
|
||||
/// Share a host directory into the VM via 9p. Format:
|
||||
/// Share a host directory into the VM via virtio-fs. Format:
|
||||
/// `<host-path>` or `<host-path>:<guest-path>`. When the guest path is
|
||||
/// omitted, the host path is used. Non-absolute guest paths are
|
||||
/// relative to the image's WORKINGDIR. Can be repeated.
|
||||
@@ -91,35 +96,55 @@ pub(crate) fn boot(
|
||||
cmdline.push(format!("boco.cmd={encoded}"));
|
||||
}
|
||||
|
||||
// Build 9p shares for each --mount. Tags are short (boco0, boco1, …)
|
||||
// because 9p mount_tag has a ~31-byte limit. The guest destination path
|
||||
// is passed on the kernel cmdline as boco.mount=<tag>:<base64(guest_path)>.
|
||||
// If no guest path is specified, the host path is used as the guest path.
|
||||
let mut virtfs_args: Vec<String> = Vec::new();
|
||||
// Build virtio-fs shares for each `--mount`. Each share gets its own
|
||||
// virtiofsd daemon under `podman unshare` (rootless UID mapping) and a
|
||||
// matching `-chardev socket` + `-device vhost-user-fs-pci` pair on the
|
||||
// QEMU side. The guest destination path is passed on the kernel cmdline
|
||||
// as `boco.mount=<tag>:<base64(guest_path)>` and resolved by /boco/init.
|
||||
let mut shares: Vec<VirtiofsShare> = Vec::with_capacity(mount.len());
|
||||
let mut fsdev_args: Vec<String> = Vec::new();
|
||||
for (i, spec) in mount.iter().enumerate() {
|
||||
let (host_path, guest_path) = match spec.split_once(':') {
|
||||
Some((h, g)) => (h, g),
|
||||
None => (spec.as_str(), spec.as_str()),
|
||||
Some((h, g)) => (h, g.to_string()),
|
||||
None => (spec.as_str(), spec.to_string()),
|
||||
};
|
||||
let canonical = std::path::Path::new(host_path)
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Cannot resolve mount path '{host_path}'"))?;
|
||||
let host_str = canonical
|
||||
let share = virtiofs::spawn_share(i, host_path, guest_path)?;
|
||||
let tag = share.tag.clone();
|
||||
let chardev = format!("char{i}");
|
||||
let socket = share
|
||||
.socket_path()
|
||||
.to_str()
|
||||
.context("Mount path is not valid UTF-8")?;
|
||||
let tag = format!("boco{i}");
|
||||
let dest_b64 = base64::engine::general_purpose::STANDARD.encode(guest_path.as_bytes());
|
||||
.context("socket path is not UTF-8")?
|
||||
.to_string();
|
||||
let dest_b64 =
|
||||
base64::engine::general_purpose::STANDARD.encode(share.guest_path.as_bytes());
|
||||
|
||||
// QEMU's QemuOpts splits on commas — escape literal commas in the
|
||||
// path as ",," per QEMU convention.
|
||||
let host_escaped = host_str.replace(',', ",,");
|
||||
virtfs_args.push("-virtfs".into());
|
||||
virtfs_args.push(format!(
|
||||
"local,path={host_escaped},mount_tag={tag},security_model=mapped-xattr"
|
||||
fsdev_args.push("-chardev".into());
|
||||
fsdev_args.push(format!("socket,id={chardev},path={socket}"));
|
||||
fsdev_args.push("-device".into());
|
||||
fsdev_args.push(format!(
|
||||
"vhost-user-fs-pci,queue-size=1024,chardev={chardev},tag={tag}"
|
||||
));
|
||||
cmdline.push(format!("boco.mount={tag}:{dest_b64}"));
|
||||
|
||||
shares.push(share);
|
||||
}
|
||||
|
||||
// vhost-user devices require shared guest RAM so virtiofsd (a separate
|
||||
// process) can DMA into it.
|
||||
let memory_args: Vec<String> = if shares.is_empty() {
|
||||
vec!["-m".into(), memory.clone()]
|
||||
} else {
|
||||
vec![
|
||||
"-m".into(),
|
||||
memory.clone(),
|
||||
"-object".into(),
|
||||
format!("memory-backend-memfd,id=mem,size={memory},share=on"),
|
||||
"-numa".into(),
|
||||
"node,memdev=mem".into(),
|
||||
]
|
||||
};
|
||||
|
||||
println!("Booting {name} from registry: {}", reg_dir.display());
|
||||
|
||||
let cmdline = cmdline.join(" ");
|
||||
@@ -136,11 +161,11 @@ pub(crate) fn boot(
|
||||
command
|
||||
.args(["-accel", "kvm"])
|
||||
.args(["-cpu", "host", "-smp", "cpus=8"])
|
||||
.args(["-m", &memory])
|
||||
.args(&memory_args)
|
||||
.arg("-kernel")
|
||||
.arg(vmlinuz_path.as_os_str())
|
||||
.args(&fs_args)
|
||||
.args(&virtfs_args)
|
||||
.args(&fsdev_args)
|
||||
.args(["-snapshot"])
|
||||
.args(["-no-reboot"])
|
||||
.args(["-append", &cmdline])
|
||||
@@ -153,6 +178,9 @@ pub(crate) fn boot(
|
||||
.status()
|
||||
.context("Failed to launch qemu-system-x86_64")?;
|
||||
|
||||
// Ensure virtiofsd daemons are torn down even on error returns below.
|
||||
drop(shares);
|
||||
|
||||
match status.code() {
|
||||
Some(0) => println!("QEMU exited cleanly."),
|
||||
Some(126) | Some(127) => {
|
||||
|
||||
Reference in new issue
Block a user