2025-10-25 20:00:32 +02:00
|
|
|
use async_trait::async_trait;
|
2025-11-01 14:56:46 +01:00
|
|
|
use miette::{IntoDiagnostic as _, Result};
|
|
|
|
|
use std::{path::PathBuf, time::Duration};
|
2025-11-01 14:44:42 +01:00
|
|
|
use tracing::info;
|
2025-10-25 20:00:32 +02:00
|
|
|
|
2026-04-07 15:56:10 +02:00
|
|
|
/// Backend tag is used internally to remember which backend handled this VM.
|
2025-10-25 20:00:32 +02:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
2025-11-01 14:56:46 +01:00
|
|
|
pub enum BackendTag {
|
|
|
|
|
Noop,
|
2026-04-07 15:46:20 +02:00
|
|
|
Qemu,
|
|
|
|
|
Propolis,
|
2025-11-01 14:56:46 +01:00
|
|
|
}
|
2025-10-25 20:00:32 +02:00
|
|
|
|
|
|
|
|
#[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>,
|
2025-11-15 18:37:30 +01:00
|
|
|
/// 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>,
|
2025-10-25 20:00:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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>,
|
2026-04-07 15:46:20 +02:00
|
|
|
/// 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>,
|
2026-04-07 21:20:15 +02:00
|
|
|
/// 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>,
|
2025-10-25 20:00:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<()>;
|
2025-11-01 18:38:17 +01:00
|
|
|
async fn suspend(&self, vm: &VmHandle) -> Result<()>;
|
2025-10-25 20:00:32 +02:00
|
|
|
async fn destroy(&self, vm: VmHandle) -> Result<()>;
|
2025-11-01 14:56:46 +01:00
|
|
|
async fn state(&self, _vm: &VmHandle) -> Result<VmState> {
|
|
|
|
|
Ok(VmState::Prepared)
|
|
|
|
|
}
|
2026-04-07 15:56:10 +02:00
|
|
|
async fn guest_ip(&self, _vm: &VmHandle) -> Result<String> {
|
2026-04-07 15:50:54 +02:00
|
|
|
Ok("127.0.0.1".to_string())
|
|
|
|
|
}
|
2025-10-25 20:00:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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);
|
2025-11-01 14:56:46 +01:00
|
|
|
tokio::fs::create_dir_all(&work_dir)
|
|
|
|
|
.await
|
|
|
|
|
.into_diagnostic()?;
|
2025-10-25 20:00:32 +02:00
|
|
|
info!(id = %id, label = %spec.label, image = ?spec.image_path, "noop prepare");
|
2025-11-01 14:56:46 +01:00
|
|
|
Ok(VmHandle {
|
|
|
|
|
id,
|
|
|
|
|
backend: BackendTag::Noop,
|
|
|
|
|
work_dir,
|
|
|
|
|
overlay_path: None,
|
|
|
|
|
seed_iso_path: None,
|
2026-04-07 15:46:20 +02:00
|
|
|
console_socket: None,
|
|
|
|
|
ssh_host_port: None,
|
|
|
|
|
mac_addr: None,
|
2026-04-07 21:20:15 +02:00
|
|
|
vcpus: 0,
|
|
|
|
|
memory_mb: 0,
|
|
|
|
|
disk_gb: None,
|
2025-11-01 14:56:46 +01:00
|
|
|
})
|
2025-10-25 20:00:32 +02:00
|
|
|
}
|
|
|
|
|
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(())
|
|
|
|
|
}
|
2025-11-01 18:38:17 +01:00
|
|
|
async fn suspend(&self, vm: &VmHandle) -> Result<()> {
|
|
|
|
|
info!(id = %vm.id, "noop suspend");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-10-25 20:00:32 +02:00
|
|
|
async fn destroy(&self, vm: VmHandle) -> Result<()> {
|
|
|
|
|
info!(id = %vm.id, "noop destroy");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|