189 lines
6.3 KiB
Rust
189 lines
6.3 KiB
Rust
//! See [`boot`]
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use base64::Engine;
|
|
use clap::Args;
|
|
use std::{fmt::Write as _, process::Command};
|
|
|
|
use crate::{
|
|
forward::PortForward,
|
|
registry::{registry_dir, vmlinuz_path},
|
|
virtiofs::{self, VirtiofsShare},
|
|
};
|
|
|
|
#[derive(Args, Debug)]
|
|
pub struct BootCmd {
|
|
/// 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<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.
|
|
#[clap(long)]
|
|
cmd: Option<String>,
|
|
|
|
/// 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.
|
|
#[clap(long)]
|
|
mount: Vec<String>,
|
|
}
|
|
|
|
/// Invoke `qemu` to boot a boco image. See [`BootCmd`] for options
|
|
///
|
|
/// This uses qemu's direct-kernel-boot with `-kernel`, so it does not require a bootloader.
|
|
/// The boco image is just an ext4 rootfs.
|
|
pub(crate) fn boot(
|
|
BootCmd {
|
|
name,
|
|
memory,
|
|
forward,
|
|
cmd,
|
|
mount,
|
|
}: BootCmd,
|
|
) -> Result<()> {
|
|
let reg_dir = registry_dir(&name)?;
|
|
let vmlinuz_path = vmlinuz_path()?;
|
|
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. Expected vmlinuz at {}",
|
|
vmlinuz_path.display()
|
|
);
|
|
}
|
|
|
|
let mut cmdline: Vec<String> = vec![
|
|
"console=ttyS0,115200".into(),
|
|
"rw".into(),
|
|
"earlyprintk=serial".into(),
|
|
"nokaslr".into(),
|
|
"init=/boco/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 `boco build` first.");
|
|
}
|
|
|
|
if let Some(cmd) = &cmd {
|
|
let argv = ["/bin/sh".to_string(), "-c".to_string(), cmd.clone()];
|
|
let data: Vec<u8> = argv.join("\0").into_bytes();
|
|
let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
|
|
cmdline.push(format!("boco.cmd={encoded}"));
|
|
}
|
|
|
|
// 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.to_string()),
|
|
None => (spec.as_str(), spec.to_string()),
|
|
};
|
|
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("socket path is not UTF-8")?
|
|
.to_string();
|
|
let dest_b64 =
|
|
base64::engine::general_purpose::STANDARD.encode(share.guest_path.as_bytes());
|
|
|
|
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(" ");
|
|
|
|
// 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(&memory_args)
|
|
.arg("-kernel")
|
|
.arg(vmlinuz_path.as_os_str())
|
|
.args(&fs_args)
|
|
.args(&fsdev_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")?;
|
|
|
|
// 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) => {
|
|
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(())
|
|
}
|