solstice-ci/crates/orchestrator/src/hypervisor.rs
2026-04-07 21:20:15 +02:00

129 lines
4 KiB
Rust

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<String>,
pub nocloud: bool,
/// Optional user-data (cloud-init NoCloud). If provided, backend will attach seed.
pub user_data: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct JobContext {
pub request_id: uuid::Uuid,
pub repo_url: String,
pub commit_sha: String,
pub workflow_job_id: Option<String>,
/// Username to SSH into the guest. If None, fall back to ExecConfig.ssh_user
pub ssh_user: Option<String>,
/// Path to private key file for this job (preferred over in-memory)
pub ssh_private_key_path: Option<String>,
/// OpenSSH-formatted private key PEM used for SSH auth for this job
pub ssh_private_key_pem: Option<String>,
/// OpenSSH-formatted public key (authorized_keys) for this job
pub ssh_public_key: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VmHandle {
pub id: String,
pub backend: BackendTag,
pub work_dir: PathBuf,
pub overlay_path: Option<PathBuf>,
pub seed_iso_path: Option<PathBuf>,
/// Console socket path (QEMU Unix socket for serial console).
pub console_socket: Option<PathBuf>,
/// Forwarded SSH port on localhost (user-mode networking).
pub ssh_host_port: Option<u16>,
/// MAC address of the VM's network interface.
pub mac_addr: Option<String>,
/// 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<u32>,
}
#[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<VmHandle>;
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<VmState> {
Ok(VmState::Prepared)
}
async fn guest_ip(&self, _vm: &VmHandle) -> Result<String> {
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<VmHandle> {
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(())
}
}