use crate::{Boot, BootSpec, Error, Lifecycle, Machine, PowerState, Result};
#[cfg(feature = "backend-tunnr")]
use crate::ImageSource;
#[cfg(feature = "backend-tunnr")]
use std::collections::HashMap;
#[cfg(feature = "backend-tunnr")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "backend-tunnr")]
type LiveHandles = Arc<Mutex<HashMap<String, tunnr_vm::BootHandle>>>;
#[derive(Default, Clone)]
pub struct KvmBoot {
#[cfg(feature = "backend-tunnr")]
live: LiveHandles,
}
impl std::fmt::Debug for KvmBoot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KvmBoot").finish_non_exhaustive()
}
}
impl KvmBoot {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "backend-tunnr")]
pub fn to_tunnr_spec(&self, spec: &BootSpec) -> Result<tunnr_vm::BootSpec> {
use std::path::PathBuf;
use std::time::Duration;
let (kernel, rootfs_or_disk) = match &spec.image {
ImageSource::KernelRootfs { kernel, rootfs } => (kernel.clone(), rootfs.clone()),
ImageSource::Disk(disk) => {
(spec.cmdline_kernel(), disk.clone())
}
other => {
return Err(Error::Spec(format!("kvm backend cannot boot {other:?}")))
}
};
let mut boot = tunnr_vm::BootSpec::smoke(PathBuf::from(kernel), PathBuf::from(rootfs_or_disk));
boot.mem_mb = spec.mem_mb;
boot.cores = spec.cores;
boot.headless = true;
if let Some(secs) = std::env::var("DRAUPNIR_VM_BOOT_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok())
{
boot.timeout = Duration::from_secs(secs);
}
if !spec.cmdline.is_empty() {
boot.kernel_cmdline = spec.cmdline.clone();
}
Ok(boot)
}
}
impl Boot for KvmBoot {
fn boot(&self, spec: &BootSpec) -> Result<Machine> {
spec.validate()?;
#[cfg(feature = "backend-tunnr")]
{
let tunnr_spec = self.to_tunnr_spec(spec)?;
let handle = tunnr_vm::boot_test(tunnr_spec)
.map_err(|e| Error::Backend(format!("tunnr boot_test: {e}")))?;
let id = handle.id().to_string();
let machine = Machine::started(&id, spec);
self.live.lock().unwrap().insert(id, handle);
Ok(machine)
}
#[cfg(not(feature = "backend-tunnr"))]
{
let _ = spec;
Err(Error::Unsupported(
"kvm backend needs the `backend-tunnr` feature (drives tunnr's KVM boot primitive)"
.into(),
))
}
}
}
impl Lifecycle for KvmBoot {
fn power_on(&self, machine: &Machine) -> Result<()> {
let _ = machine;
Err(Error::Unsupported(
"kvm/tunnr VMs are fire-and-forget: re-power by calling draupnir::boot() again".into(),
))
}
fn power_off(&self, machine: &Machine) -> Result<()> {
#[cfg(feature = "backend-tunnr")]
{
let handle = self
.live
.lock()
.unwrap()
.remove(&machine.id)
.ok_or_else(|| Error::Backend(format!("no live tunnr VM for {}", machine.id)))?;
handle.kill();
Ok(())
}
#[cfg(not(feature = "backend-tunnr"))]
{
let _ = machine;
Err(Error::Unsupported(
"kvm backend needs the `backend-tunnr` feature".into(),
))
}
}
fn status(&self, machine: &Machine) -> Result<PowerState> {
#[cfg(feature = "backend-tunnr")]
{
let guard = self.live.lock().unwrap();
let Some(handle) = guard.get(&machine.id) else {
return Ok(PowerState::Unknown);
};
Ok(map_status(handle.poll_status()))
}
#[cfg(not(feature = "backend-tunnr"))]
{
let _ = machine;
Err(Error::Unsupported(
"kvm backend needs the `backend-tunnr` feature".into(),
))
}
}
}
#[cfg(feature = "backend-tunnr")]
fn map_status(s: tunnr_vm::BootStatus) -> PowerState {
match s {
tunnr_vm::BootStatus::Booting | tunnr_vm::BootStatus::BootedOk => PowerState::On,
tunnr_vm::BootStatus::Failed(_)
| tunnr_vm::BootStatus::Killed
| tunnr_vm::BootStatus::TimedOut => PowerState::Off,
}
}
#[cfg(feature = "backend-tunnr")]
impl BootSpec {
fn cmdline_kernel(&self) -> String {
String::new()
}
}
#[cfg(all(test, feature = "backend-tunnr"))]
mod tests {
use super::*;
use crate::ImageSource;
#[test]
fn kvm_spec_maps_kernel_rootfs_mem_and_cores_onto_tunnr() {
let mut spec = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz");
spec.mem_mb = 1024;
spec.cores = 4;
spec.cmdline = "korp.smoke=1".into();
let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
assert_eq!(t.kernel, std::path::PathBuf::from("/bzImage"));
assert_eq!(t.rootfs_or_disk, std::path::PathBuf::from("/rootfs.cpio.gz"));
assert_eq!(t.mem_mb, 1024);
assert_eq!(t.cores, 4);
assert!(t.headless);
assert_eq!(t.kernel_cmdline, "korp.smoke=1");
}
#[test]
fn kvm_spec_rejects_a_non_kvm_image() {
let mut spec = BootSpec::kvm_kernel_rootfs("bad", "/k", "/r");
spec.image = ImageSource::OciImage("redis:7".into());
assert!(matches!(KvmBoot::new().to_tunnr_spec(&spec), Err(Error::Spec(_))));
}
#[test]
fn status_of_an_unknown_machine_is_unknown() {
let kvm = KvmBoot::new();
let m = Machine {
id: "boot-does-not-exist".into(),
spec_name: "x".into(),
backend: crate::Backend::Kvm,
power: PowerState::Unknown,
};
assert_eq!(kvm.status(&m).unwrap(), PowerState::Unknown);
}
}