use async_trait::async_trait; use miette::{IntoDiagnostic as _, Result}; use std::{path::PathBuf, time::Duration}; use tracing::info; /// Backend tag is used internally to remember which backend handled this VM. #[derive(Debug, Clone, Copy)] pub enum BackendTag { Noop, Qemu, Propolis, } #[derive(Debug, Clone)] pub struct VmSpec { pub label: String, pub image_path: PathBuf, pub cpu: u16, pub ram_mb: u32, pub disk_gb: u32, pub network: Option, pub nocloud: bool, /// Optional user-data (cloud-init NoCloud). If provided, backend will attach seed. pub user_data: Option>, } #[derive(Debug, Clone)] pub struct JobContext { pub request_id: uuid::Uuid, pub repo_url: String, pub commit_sha: String, pub workflow_job_id: Option, /// Username to SSH into the guest. If None, fall back to ExecConfig.ssh_user pub ssh_user: Option, /// Path to private key file for this job (preferred over in-memory) pub ssh_private_key_path: Option, /// OpenSSH-formatted private key PEM used for SSH auth for this job pub ssh_private_key_pem: Option, /// OpenSSH-formatted public key (authorized_keys) for this job pub ssh_public_key: Option, } #[derive(Debug, Clone)] pub struct VmHandle { pub id: String, pub backend: BackendTag, pub work_dir: PathBuf, pub overlay_path: Option, pub seed_iso_path: Option, /// Console socket path (QEMU Unix socket for serial console). pub console_socket: Option, /// Forwarded SSH port on localhost (user-mode networking). pub ssh_host_port: Option, /// MAC address of the VM's network interface. pub mac_addr: Option, /// vCPU count (preserved from prepare for start). pub vcpus: u16, /// Memory in MB (preserved from prepare for start). pub memory_mb: u64, /// Disk size in GB. pub disk_gb: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VmState { Prepared, Running, Stopped, } #[async_trait] pub trait Hypervisor: Send + Sync { async fn prepare(&self, spec: &VmSpec, ctx: &JobContext) -> Result; async fn start(&self, vm: &VmHandle) -> Result<()>; async fn stop(&self, vm: &VmHandle, graceful_timeout: Duration) -> Result<()>; async fn suspend(&self, vm: &VmHandle) -> Result<()>; async fn destroy(&self, vm: VmHandle) -> Result<()>; async fn state(&self, _vm: &VmHandle) -> Result { Ok(VmState::Prepared) } async fn guest_ip(&self, _vm: &VmHandle) -> Result { Ok("127.0.0.1".to_string()) } } /// No-op hypervisor for development on hosts without privileges. #[derive(Debug, Clone, Default)] pub struct NoopHypervisor; #[async_trait] impl Hypervisor for NoopHypervisor { async fn prepare(&self, spec: &VmSpec, ctx: &JobContext) -> Result { let id = format!("noop-{}", ctx.request_id); let work_dir = std::env::temp_dir().join("solstice-noop").join(&id); tokio::fs::create_dir_all(&work_dir) .await .into_diagnostic()?; info!(id = %id, label = %spec.label, image = ?spec.image_path, "noop prepare"); Ok(VmHandle { id, backend: BackendTag::Noop, work_dir, overlay_path: None, seed_iso_path: None, console_socket: None, ssh_host_port: None, mac_addr: None, vcpus: 0, memory_mb: 0, disk_gb: None, }) } async fn start(&self, vm: &VmHandle) -> Result<()> { info!(id = %vm.id, "noop start"); Ok(()) } async fn stop(&self, vm: &VmHandle, _t: Duration) -> Result<()> { info!(id = %vm.id, "noop stop"); Ok(()) } async fn suspend(&self, vm: &VmHandle) -> Result<()> { info!(id = %vm.id, "noop suspend"); Ok(()) } async fn destroy(&self, vm: VmHandle) -> Result<()> { info!(id = %vm.id, "noop destroy"); Ok(()) } }