use std::path::PathBuf;
use arcbox_vm_driver::{
BootSpec, CacheMode, ConsoleSpec, DiskSpec, IsolationSpec, NicSpec, RestoreSpec, VmId, VmSpec,
VsockSpec,
};
use crate::boot_proto::KernelIpParam;
use crate::error::{Result, VmmError};
use crate::sandbox::{NetworkAttachment, SandboxSpec, ipv4, netmask};
pub fn build_vm_spec(
id: &str,
spec: &SandboxSpec,
net: Option<&NetworkAttachment>,
kernel: PathBuf,
rootfs: PathBuf,
isolation: IsolationSpec,
) -> Result<VmSpec> {
let cmdline = match net {
Some(net) if !spec.boot_args.contains("ip=") => {
let ip_param = KernelIpParam {
client: ipv4(net.identity.ip)?,
gateway: ipv4(net.identity.gateway)?,
netmask: netmask(net.identity.prefix_len),
};
format!("{} {ip_param}", spec.boot_args)
}
_ => spec.boot_args.clone(),
};
Ok(VmSpec {
id: VmId::new(id)?,
cpus: spec.vcpus.max(1),
memory_mib: u32::try_from(spec.memory_mib).map_err(|_| {
VmmError::Config(format!(
"memory_mib {} exceeds what a VM spec can carry",
spec.memory_mib
))
})?,
boot: BootSpec::Kernel {
image: kernel,
cmdline,
initrd: None,
},
disks: vec![rootfs_disk(rootfs)],
nics: net.map(|net| net.nic.clone()).into_iter().collect(),
vsock: Some(VsockSpec { guest_cid: 3 }),
shares: Vec::new(),
console: ConsoleSpec::Off,
balloon: false,
entropy: false,
dirty_tracking: true,
isolation,
})
}
pub const ROOTFS_DISK_ID: &str = "rootfs";
pub(super) fn rootfs_disk(path: PathBuf) -> DiskSpec {
DiskSpec {
id: ROOTFS_DISK_ID.into(),
path,
read_only: false,
root: true,
cache: CacheMode::Unsafe,
}
}
pub fn restore_spec(
owner: &str,
rootfs: PathBuf,
nic: Option<NicSpec>,
isolation: IsolationSpec,
) -> Result<RestoreSpec> {
Ok(RestoreSpec {
id: VmId::new(owner)?,
nics: nic.into_iter().collect(),
disks: vec![rootfs_disk(rootfs)],
isolation,
})
}
#[cfg(test)]
mod tests {
use std::path::Path;
use arcbox_vm_driver::NicAttachment;
use arcbox_vm_driver::net::{NetworkIdentity, NetworkLease};
use super::*;
const GUEST_IP: std::net::Ipv4Addr = std::net::Ipv4Addr::new(169, 254, 100, 2);
const GUEST_GATEWAY: std::net::Ipv4Addr = std::net::Ipv4Addr::new(169, 254, 100, 1);
const GUEST_PREFIX_LEN: u8 = 30;
const GUEST_NETMASK: std::net::Ipv4Addr = std::net::Ipv4Addr::new(255, 255, 255, 252);
fn attachment() -> NetworkAttachment {
NetworkAttachment {
lease: NetworkLease {
vm: VmId::new("box").unwrap(),
ip: "172.20.0.7".parse().unwrap(),
prefix_len: 16,
gateway: "172.20.0.1".parse().unwrap(),
mac: "02:fc:00:00:00:07".parse().unwrap(),
cleanup_token: "gen-1".into(),
},
nic: NicSpec {
id: "eth0".into(),
mac: "02:fc:00:00:00:07".parse().unwrap(),
attachment: NicAttachment::Tap {
name: "vmtap0-7".into(),
},
},
identity: NetworkIdentity {
ip: GUEST_IP.into(),
prefix_len: GUEST_PREFIX_LEN,
gateway: GUEST_GATEWAY.into(),
dns: vec![GUEST_GATEWAY.into()],
mac: "02:fc:00:00:00:07".parse().unwrap(),
},
invariant_identity: true,
}
}
#[test]
fn boot_spec_bakes_the_invariant_identity_and_the_fixed_devices() {
let spec = SandboxSpec {
boot_args: "console=ttyS0".into(),
vcpus: 0,
memory_mib: 512,
..SandboxSpec::default()
};
let vm = build_vm_spec(
"box",
&spec,
Some(&attachment()),
PathBuf::from("/jail/vmlinux"),
PathBuf::from("/jail/rootfs.ext4"),
IsolationSpec::None,
)
.unwrap();
assert_eq!(vm.id.as_str(), "box");
assert_eq!((vm.cpus, vm.memory_mib), (1, 512));
let BootSpec::Kernel { image, cmdline, .. } = &vm.boot else {
panic!("a direct kernel boot");
};
assert_eq!(image, Path::new("/jail/vmlinux"));
assert!(cmdline.starts_with("console=ttyS0 ip="), "{cmdline}");
assert!(cmdline.contains(&GUEST_IP.to_string()), "{cmdline}");
assert!(cmdline.contains(&GUEST_NETMASK.to_string()), "{cmdline}");
assert_eq!(netmask(GUEST_PREFIX_LEN), GUEST_NETMASK);
assert_eq!(vm.disks.len(), 1);
assert!(vm.disks[0].root && !vm.disks[0].read_only);
assert_eq!(vm.disks[0].path, Path::new("/jail/rootfs.ext4"));
assert_eq!(vm.nics.len(), 1);
assert_eq!(vm.nics[0].mac.to_string(), "02:fc:00:00:00:07");
assert_eq!(
vm.nics[0].attachment,
NicAttachment::Tap {
name: "vmtap0-7".into()
}
);
assert_eq!(vm.vsock.map(|v| v.guest_cid), Some(3));
assert!(vm.dirty_tracking && !vm.balloon && !vm.entropy);
assert_eq!(vm.console, ConsoleSpec::Off);
vm.validate()
.expect("the driver accepts what the manager builds");
let pinned = SandboxSpec {
boot_args: "ip=10.0.0.2::10.0.0.1:255.255.255.0".into(),
..spec.clone()
};
let vm = build_vm_spec(
"box",
&pinned,
Some(&attachment()),
"/k".into(),
"/r".into(),
IsolationSpec::None,
)
.unwrap();
let BootSpec::Kernel { cmdline, .. } = &vm.boot else {
unreachable!()
};
assert_eq!(cmdline, &pinned.boot_args);
let vm = build_vm_spec(
"box",
&spec,
None,
"/k".into(),
"/r".into(),
IsolationSpec::None,
)
.unwrap();
assert_eq!(vm.nics, []);
let BootSpec::Kernel { cmdline, .. } = &vm.boot else {
unreachable!()
};
assert_eq!(cmdline, "console=ttyS0");
}
}