//! QEMU: boot a registry entry with qemu-system-x86_64 using direct kernel //! boot (-kernel/-initrd or -drive). The kernel is shared across all VMs //! at $XDG_DATA_HOME/slim-rs/registry/vmlinuz. A universal /slim/init script (injected //! at build time) handles VM bootstrap; /slim/exec runs the container's //! CMD/ENTRYPOINT. Runtime overrides are delivered as `slim.cmd=` //! on the kernel cmdline. use anyhow::{Context, Result, bail}; use base64::Engine; use clap::Args; use std::{fmt::Write as _, path::PathBuf, process::Command}; use crate::{ forward::PortForward, registry::{registry_base_dir, registry_dir}, }; #[derive(Args, Debug)] pub struct RunCmd { /// Image tag / registry subdir name name: String, /// Amount of memory to give the VM, in qemu's format. #[clap(short, long, default_value = "1024M")] memory: String, /// Forward ports from host to guest. Example: `tcp:0.0.0.0:80-:8080` #[clap(long)] forward: Vec, /// Override the command to exec in the VM (base64-encoded on the kernel /// cmdline as slim.cmd=). Overrides the CMD inferred at build time. #[clap(long)] cmd: Option, /// 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, } pub(crate) fn run( RunCmd { name, memory, forward, cmd, mount, }: RunCmd, ) -> Result<()> { let reg_dir = registry_dir(&name)?; let vmlinuz_path = registry_base_dir()?.join("vmlinuz"); let initrd_path = reg_dir.join("initrd"); let qcow2_path = reg_dir.join("image.qcow2"); if !vmlinuz_path.exists() { // TODO: guide user in how to set up a kernel bail!( "No kernel available. Place a vmlinuz at {}", vmlinuz_path.display() ); } let mut cmdline: Vec = vec![ "console=ttyS0,115200".into(), "rw".into(), "earlyprintk=serial".into(), "nokaslr".into(), "init=/slim/init".into(), "devtmpfs.mount=1".into(), // Automatically mount /dev at boot ]; let fs_args; let qcow2_arg = format!("file={},format=qcow2,if=virtio", qcow2_path.display()); if qcow2_path.exists() { fs_args = vec!["-drive".to_string(), qcow2_arg]; cmdline.extend(["root=/dev/vda".to_string(), "rootfstype=ext4".to_string()]); } else if initrd_path.exists() { let initrd_str = initrd_path.to_str().context("Invalid UTF-8")?.to_string(); fs_args = vec!["-initrd".to_string(), initrd_str]; cmdline.push("root=/dev/ram0".into()); } else { bail!("Registry missing rootfs/initrd for '{name}'. Run `slim build` first."); } if let Some(cmd) = &cmd { let encoded = base64::engine::general_purpose::STANDARD.encode(cmd.as_bytes()); 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=:. let mut virtfs_args: Vec = 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()); // QEMU's QemuOpts splits on commas — escape literal commas in the // path as ",," per QEMU convention. let path_escaped = path_str.replace(',', ",,"); virtfs_args.push("-virtfs".into()); virtfs_args.push(format!( "local,path={path_escaped},mount_tag={tag},security_model=mapped-xattr" )); cmdline.push(format!("slim.mount={tag}:{dest_b64}")); } println!("Booting {name} from registry: {}", reg_dir.display()); let cmdline = cmdline.join(" "); // add NIC and forward any user specified ports let mut network = "user,model=virtio-net-pci".to_string(); for forward in &forward { _ = write!(&mut network, ",hostfwd={forward}"); } let mut command = Command::new("qemu-system-x86_64"); // TODO: make a lot of this configurable. especially -cpus command .args(["-accel", "kvm"]) .args(["-cpu", "host", "-smp", "cpus=8"]) .args(["-m", &memory]) .arg("-kernel") .arg(vmlinuz_path.as_os_str()) .args(&fs_args) .args(&virtfs_args) .args(["-snapshot"]) .args(["-no-reboot"]) .args(["-append", &cmdline]) .args(["-nographic"]) .args(["-nic", &network]); println!("{command:?}"); let status = command .status() .context("Failed to launch qemu-system-x86_64")?; match status.code() { Some(0) => println!("QEMU exited cleanly."), Some(126) | Some(127) => { bail!("Failed to start qemu-system-x86_64 (is it installed and in PATH?)") } Some(code) => bail!("QEMU exited with code {code}"), None => bail!("QEMU terminated by a signal"), } Ok(()) }