use std::fmt;
pub const DEFAULT_CMDLINE: &str = "console=ttyAMA0 reboot=t panic=-1";
pub const DEFAULT_MEMORY_MIB: usize = 256;
pub const DEFAULT_VCPUS: u32 = 1;
pub const THROUGHPUT_PROFILE_VCPUS: u32 = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VmProfile {
Latency,
Throughput,
}
impl VmProfile {
pub fn parse(s: &str) -> Option<Self> {
match s {
"latency" | "low-latency" => Some(Self::Latency),
"throughput" => Some(Self::Throughput),
_ => None,
}
}
pub fn default_vcpus(self) -> u32 {
match self {
Self::Latency => DEFAULT_VCPUS,
Self::Throughput => THROUGHPUT_PROFILE_VCPUS,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VmResources {
pub kernel_path: Option<String>,
pub initrd_path: Option<String>,
pub cmdline: String,
pub memory_mib: usize,
pub block_devices: Vec<String>,
pub volumes: Vec<VolumeSpec>,
pub vcpus: u32,
pub restore_from: Option<String>,
pub cow_restore: bool,
pub snapshot: SnapshotResources,
pub endpoints: EndpointResources,
pub balloon_target_pages: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VolumeSpec {
pub host_path: String,
pub guest_path: String,
pub size_bytes: u64,
}
impl VolumeSpec {
pub const DEFAULT_SIZE_BYTES: u64 = 1024 * 1024 * 1024;
pub fn new(host_path: impl Into<String>, guest_path: impl Into<String>) -> Self {
Self {
host_path: host_path.into(),
guest_path: guest_path.into(),
size_bytes: Self::DEFAULT_SIZE_BYTES,
}
}
pub fn with_size_bytes(mut self, size_bytes: u64) -> Self {
self.size_bytes = size_bytes;
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SnapshotResources {
pub after_ms: Option<u64>,
pub at_heartbeat: Option<u64>,
pub on_listener: bool,
pub quiesce_ms: u64,
pub out_path: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EndpointResources {
pub vsock_mux: Option<String>,
pub http_port: Option<String>,
pub vsock_mux_handoff: Option<String>,
pub vsock_exec: Option<String>,
pub vsock_exec_guest_port: Option<u32>,
}
pub const DEFAULT_EXEC_GUEST_PORT: u32 = 1028;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResourceError {
MissingKernel,
ZeroMemory,
ZeroVcpus,
SnapshotTriggerWithoutOutput,
}
impl VmResources {
pub fn new() -> Self {
Self::default()
}
pub fn for_kernel(kernel_path: impl Into<String>, initrd_path: impl Into<String>) -> Self {
Self::new()
.with_kernel_path(kernel_path)
.with_initramfs(initrd_path)
}
pub fn from_snapshot(path: impl Into<String>) -> Self {
Self::new().with_restore(path)
}
pub fn with_kernel_path(mut self, path: impl Into<String>) -> Self {
self.kernel_path = Some(path.into());
self
}
pub fn with_initramfs(mut self, path: impl Into<String>) -> Self {
self.initrd_path = Some(path.into());
self
}
pub fn with_cmdline(mut self, cmdline: impl Into<String>) -> Self {
self.cmdline = cmdline.into();
self
}
pub fn with_memory_mib(mut self, memory_mib: usize) -> Self {
self.memory_mib = memory_mib;
self
}
pub fn with_profile(mut self, profile: VmProfile) -> Self {
self.apply_profile_defaults(profile);
self
}
pub fn with_vcpus(mut self, vcpus: u32) -> Self {
self.vcpus = vcpus;
self
}
pub fn with_block_device(mut self, path: impl Into<String>) -> Self {
self.block_devices.push(path.into());
self
}
pub fn with_volume(mut self, volume: VolumeSpec) -> Self {
self.volumes.push(volume);
self
}
pub fn with_restore(mut self, path: impl Into<String>) -> Self {
self.restore_from = Some(path.into());
self
}
pub fn with_cow_restore(mut self, enabled: bool) -> Self {
self.cow_restore = enabled;
self
}
pub fn with_snapshot_after_ms(mut self, after_ms: u64, out_path: impl Into<String>) -> Self {
self.snapshot.after_ms = Some(after_ms);
self.snapshot.out_path = Some(out_path.into());
self
}
pub fn with_snapshot_at_heartbeat(
mut self,
at_heartbeat: u64,
out_path: impl Into<String>,
) -> Self {
self.snapshot.at_heartbeat = Some(at_heartbeat);
self.snapshot.out_path = Some(out_path.into());
self
}
pub fn with_snapshot_on_listener(mut self, out_path: impl Into<String>) -> Self {
self.snapshot.on_listener = true;
self.snapshot.out_path = Some(out_path.into());
self
}
pub fn with_quiesce_ms(mut self, quiesce_ms: u64) -> Self {
self.snapshot.quiesce_ms = quiesce_ms;
self
}
pub fn with_vsock_mux(mut self, path: impl Into<String>) -> Self {
self.endpoints.vsock_mux = Some(path.into());
self
}
pub fn with_http_port(mut self, port: impl Into<String>) -> Self {
self.endpoints.http_port = Some(port.into());
self
}
pub fn with_vsock_mux_handoff(mut self, path: impl Into<String>) -> Self {
self.endpoints.vsock_mux_handoff = Some(path.into());
self
}
pub fn with_vsock_exec(mut self, path: impl Into<String>) -> Self {
self.endpoints.vsock_exec = Some(path.into());
self
}
pub fn with_vsock_exec_guest_port(mut self, port: u32) -> Self {
self.endpoints.vsock_exec_guest_port = Some(port);
self
}
pub fn memory_bytes(&self) -> usize {
self.memory_mib * 1024 * 1024
}
pub fn is_restore(&self) -> bool {
self.restore_from.is_some()
}
pub fn apply_profile_defaults(&mut self, profile: VmProfile) {
self.vcpus = profile.default_vcpus();
}
pub fn validate_for_run(&self) -> Result<(), ResourceError> {
if self.memory_mib == 0 {
return Err(ResourceError::ZeroMemory);
}
if self.vcpus == 0 {
return Err(ResourceError::ZeroVcpus);
}
if self.kernel_path.is_none() && self.restore_from.is_none() {
return Err(ResourceError::MissingKernel);
}
let wants_snapshot = self.snapshot.after_ms.is_some()
|| self.snapshot.at_heartbeat.is_some()
|| self.snapshot.on_listener;
if wants_snapshot && self.snapshot.out_path.is_none() {
return Err(ResourceError::SnapshotTriggerWithoutOutput);
}
Ok(())
}
}
impl Default for VmResources {
fn default() -> Self {
Self {
kernel_path: None,
initrd_path: None,
cmdline: DEFAULT_CMDLINE.to_string(),
memory_mib: DEFAULT_MEMORY_MIB,
block_devices: Vec::new(),
volumes: Vec::new(),
vcpus: DEFAULT_VCPUS,
restore_from: None,
cow_restore: false,
snapshot: SnapshotResources::default(),
endpoints: EndpointResources::default(),
balloon_target_pages: None,
}
}
}
impl fmt::Display for ResourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResourceError::MissingKernel => {
write!(f, "kernel path or restore snapshot is required")
}
ResourceError::ZeroMemory => write!(f, "memory must be greater than zero"),
ResourceError::ZeroVcpus => write!(f, "vCPU count must be greater than zero"),
ResourceError::SnapshotTriggerWithoutOutput => {
write!(f, "snapshot trigger requires snapshot output path")
}
}
}
}
impl std::error::Error for ResourceError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_supermachine_cli() {
let resources = VmResources::default();
assert_eq!(resources.cmdline, DEFAULT_CMDLINE);
assert_eq!(resources.memory_mib, 256);
assert_eq!(resources.vcpus, 1);
assert_eq!(resources.memory_bytes(), 256 * 1024 * 1024);
}
#[test]
fn profile_defaults_are_stable() {
assert_eq!(VmProfile::parse("latency"), Some(VmProfile::Latency));
assert_eq!(VmProfile::parse("low-latency"), Some(VmProfile::Latency));
assert_eq!(VmProfile::parse("throughput"), Some(VmProfile::Throughput));
assert_eq!(VmProfile::parse("unknown"), None);
assert_eq!(VmProfile::Latency.default_vcpus(), 1);
assert_eq!(VmProfile::Throughput.default_vcpus(), 4);
}
#[test]
fn applies_profile_defaults_to_resources() {
let mut resources = VmResources::default();
resources.apply_profile_defaults(VmProfile::Throughput);
assert_eq!(resources.vcpus, 4);
resources.apply_profile_defaults(VmProfile::Latency);
assert_eq!(resources.vcpus, 1);
}
#[test]
fn convenience_constructors_cover_kernel_and_restore() {
let kernel = VmResources::for_kernel("kernel", "initrd");
assert_eq!(kernel.kernel_path.as_deref(), Some("kernel"));
assert_eq!(kernel.initrd_path.as_deref(), Some("initrd"));
assert!(!kernel.is_restore());
let restore = VmResources::from_snapshot("snap.sm");
assert_eq!(restore.restore_from.as_deref(), Some("snap.sm"));
assert!(restore.is_restore());
}
#[test]
fn builder_style_methods_cover_common_library_config() {
let resources = VmResources::new()
.with_kernel_path("kernel")
.with_initramfs("initrd")
.with_cmdline("console=ttyS0")
.with_memory_mib(512)
.with_profile(VmProfile::Throughput)
.with_vcpus(2)
.with_block_device("rootfs.squashfs")
.with_snapshot_at_heartbeat(1, "snap.sm")
.with_quiesce_ms(7)
.with_vsock_mux("/tmp/vsock.sock")
.with_http_port("8080");
assert_eq!(resources.kernel_path.as_deref(), Some("kernel"));
assert_eq!(resources.initrd_path.as_deref(), Some("initrd"));
assert_eq!(resources.cmdline, "console=ttyS0");
assert_eq!(resources.memory_mib, 512);
assert_eq!(resources.vcpus, 2);
assert_eq!(resources.block_devices, vec!["rootfs.squashfs"]);
assert_eq!(resources.snapshot.at_heartbeat, Some(1));
assert_eq!(resources.snapshot.quiesce_ms, 7);
assert_eq!(resources.snapshot.out_path.as_deref(), Some("snap.sm"));
assert_eq!(
resources.endpoints.vsock_mux.as_deref(),
Some("/tmp/vsock.sock")
);
assert_eq!(resources.endpoints.http_port.as_deref(), Some("8080"));
}
#[test]
fn builder_style_restore_config_is_valid_without_kernel() {
let resources = VmResources::new()
.with_restore("snap.sm")
.with_cow_restore(true);
assert!(resources.is_restore());
assert!(resources.cow_restore);
assert_eq!(resources.validate_for_run(), Ok(()));
}
#[test]
fn validates_kernel_or_restore() {
let mut resources = VmResources::default();
assert_eq!(
resources.validate_for_run(),
Err(ResourceError::MissingKernel)
);
resources.kernel_path = Some("vmlinux".to_string());
assert_eq!(resources.validate_for_run(), Ok(()));
resources.kernel_path = None;
resources.restore_from = Some("snap.sm".to_string());
assert_eq!(resources.validate_for_run(), Ok(()));
}
}