Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
88
vendor/ruvector/crates/rvf/rvf-launch/src/error.rs
vendored
Normal file
88
vendor/ruvector/crates/rvf/rvf-launch/src/error.rs
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Error types for the RVF launcher.
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// All errors that the launcher can produce.
|
||||
#[derive(Debug)]
|
||||
pub enum LaunchError {
|
||||
/// QEMU binary not found on the system.
|
||||
QemuNotFound { searched: Vec<String> },
|
||||
/// KVM is required but not available.
|
||||
KvmRequired,
|
||||
/// The RVF file does not contain a KERNEL_SEG.
|
||||
NoKernelSegment { path: PathBuf },
|
||||
/// Failed to extract kernel from the RVF file.
|
||||
KernelExtraction(String),
|
||||
/// Failed to create a temporary file for the extracted kernel.
|
||||
TempFile(io::Error),
|
||||
/// QEMU process failed to start.
|
||||
QemuSpawn(io::Error),
|
||||
/// QEMU process exited with a non-zero code.
|
||||
QemuExited { code: Option<i32>, stderr: String },
|
||||
/// Timeout waiting for the VM to become ready.
|
||||
Timeout { seconds: u64 },
|
||||
/// QMP protocol error.
|
||||
Qmp(String),
|
||||
/// I/O error communicating with QMP socket.
|
||||
QmpIo(io::Error),
|
||||
/// Port is already in use.
|
||||
PortInUse { port: u16 },
|
||||
/// The VM process has already exited.
|
||||
VmNotRunning,
|
||||
/// Generic I/O error.
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for LaunchError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::QemuNotFound { searched } => {
|
||||
write!(f, "QEMU not found; searched: {}", searched.join(", "))
|
||||
}
|
||||
Self::KvmRequired => {
|
||||
write!(
|
||||
f,
|
||||
"KVM is required by kernel flags but /dev/kvm is not accessible"
|
||||
)
|
||||
}
|
||||
Self::NoKernelSegment { path } => {
|
||||
write!(f, "no KERNEL_SEG found in {}", path.display())
|
||||
}
|
||||
Self::KernelExtraction(msg) => write!(f, "kernel extraction failed: {msg}"),
|
||||
Self::TempFile(e) => write!(f, "failed to create temp file: {e}"),
|
||||
Self::QemuSpawn(e) => write!(f, "failed to spawn QEMU: {e}"),
|
||||
Self::QemuExited { code, stderr } => {
|
||||
write!(f, "QEMU exited with code {code:?}")?;
|
||||
if !stderr.is_empty() {
|
||||
write!(f, ": {stderr}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::Timeout { seconds } => {
|
||||
write!(f, "VM did not become ready within {seconds}s")
|
||||
}
|
||||
Self::Qmp(msg) => write!(f, "QMP error: {msg}"),
|
||||
Self::QmpIo(e) => write!(f, "QMP I/O error: {e}"),
|
||||
Self::PortInUse { port } => write!(f, "port {port} is already in use"),
|
||||
Self::VmNotRunning => write!(f, "VM process is not running"),
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LaunchError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::TempFile(e) | Self::QemuSpawn(e) | Self::QmpIo(e) | Self::Io(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for LaunchError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
200
vendor/ruvector/crates/rvf/rvf-launch/src/extract.rs
vendored
Normal file
200
vendor/ruvector/crates/rvf/rvf-launch/src/extract.rs
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
//! Kernel and initramfs extraction from RVF files.
|
||||
//!
|
||||
//! Opens the RVF store read-only, locates the KERNEL_SEG, parses the
|
||||
//! KernelHeader, and writes the kernel image and optional initramfs to
|
||||
//! temporary files that persist until the returned handles are dropped.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rvf_runtime::RvfStore;
|
||||
use rvf_types::kernel::KernelHeader;
|
||||
|
||||
use crate::error::LaunchError;
|
||||
|
||||
/// Extracted kernel artifacts ready for QEMU consumption.
|
||||
pub struct ExtractedKernel {
|
||||
/// Path to the extracted kernel image (bzImage or equivalent).
|
||||
pub kernel_path: PathBuf,
|
||||
/// Path to the extracted initramfs, if present in the KERNEL_SEG.
|
||||
pub initramfs_path: Option<PathBuf>,
|
||||
/// The parsed KernelHeader.
|
||||
pub header: KernelHeader,
|
||||
/// The kernel command line string.
|
||||
pub cmdline: String,
|
||||
/// Temp directory holding the extracted files (kept alive via ownership).
|
||||
_tempdir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ExtractedKernel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ExtractedKernel")
|
||||
.field("kernel_path", &self.kernel_path)
|
||||
.field("initramfs_path", &self.initramfs_path)
|
||||
.field("cmdline", &self.cmdline)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract kernel and initramfs from an RVF file to temporary files.
|
||||
///
|
||||
/// The returned `ExtractedKernel` owns a `TempDir`; the files are cleaned
|
||||
/// up when it is dropped.
|
||||
pub fn extract_kernel(rvf_path: &Path) -> Result<ExtractedKernel, LaunchError> {
|
||||
let store = RvfStore::open_readonly(rvf_path)
|
||||
.map_err(|e| LaunchError::KernelExtraction(format!("failed to open store: {e:?}")))?;
|
||||
|
||||
let (header_bytes, remainder) = store
|
||||
.extract_kernel()
|
||||
.map_err(|e| LaunchError::KernelExtraction(format!("segment read error: {e:?}")))?
|
||||
.ok_or_else(|| LaunchError::NoKernelSegment {
|
||||
path: rvf_path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
if header_bytes.len() < 128 {
|
||||
return Err(LaunchError::KernelExtraction(
|
||||
"KernelHeader too short".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut hdr_array = [0u8; 128];
|
||||
hdr_array.copy_from_slice(&header_bytes[..128]);
|
||||
let header = KernelHeader::from_bytes(&hdr_array)
|
||||
.map_err(|e| LaunchError::KernelExtraction(format!("bad KernelHeader: {e:?}")))?;
|
||||
|
||||
// The wire format after the 128-byte KernelHeader (which is already
|
||||
// split off into `header_bytes`) is:
|
||||
//
|
||||
// For simple embed_kernel (no binding):
|
||||
// kernel_image || cmdline
|
||||
//
|
||||
// For embed_kernel_with_binding:
|
||||
// KernelBinding(128) || cmdline || kernel_image
|
||||
//
|
||||
// We determine the layout from header.image_size which tells us the
|
||||
// kernel image length. The cmdline is header.cmdline_length bytes.
|
||||
|
||||
let image_size = header.image_size as usize;
|
||||
let cmdline_length = header.cmdline_length as usize;
|
||||
|
||||
// Simple format: image comes first in the remainder, then cmdline
|
||||
let (kernel_image, cmdline) = if image_size > 0 && image_size <= remainder.len() {
|
||||
let img = &remainder[..image_size];
|
||||
let cmd = if cmdline_length > 0 && image_size + cmdline_length <= remainder.len() {
|
||||
String::from_utf8_lossy(&remainder[image_size..image_size + cmdline_length])
|
||||
.into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
(img, cmd)
|
||||
} else {
|
||||
// Fallback: treat entire remainder as kernel image
|
||||
(&remainder[..], String::new())
|
||||
};
|
||||
|
||||
// Write to temp files
|
||||
let tempdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
|
||||
|
||||
let kernel_file_path = tempdir.path().join("vmlinuz");
|
||||
{
|
||||
let mut f = std::fs::File::create(&kernel_file_path).map_err(LaunchError::TempFile)?;
|
||||
f.write_all(kernel_image).map_err(LaunchError::TempFile)?;
|
||||
f.sync_all().map_err(LaunchError::TempFile)?;
|
||||
}
|
||||
|
||||
// For now we do not split out a separate initramfs from the kernel
|
||||
// image. A future version could detect an appended initramfs using
|
||||
// the standard Linux trailer magic (0x6d65736800000000).
|
||||
let initramfs_path = None;
|
||||
|
||||
Ok(ExtractedKernel {
|
||||
kernel_path: kernel_file_path,
|
||||
initramfs_path,
|
||||
header,
|
||||
cmdline,
|
||||
_tempdir: tempdir,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rvf_runtime::options::RvfOptions;
|
||||
use rvf_types::kernel::KernelArch;
|
||||
|
||||
#[test]
|
||||
fn extract_from_store_with_kernel() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let rvf_path = dir.path().join("test.rvf");
|
||||
|
||||
let opts = RvfOptions {
|
||||
dimension: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
|
||||
|
||||
let image = b"MZ\x00fake-kernel-image-for-testing";
|
||||
store
|
||||
.embed_kernel(
|
||||
KernelArch::X86_64 as u8,
|
||||
0x01,
|
||||
0,
|
||||
image,
|
||||
8080,
|
||||
Some("console=ttyS0"),
|
||||
)
|
||||
.unwrap();
|
||||
store.close().unwrap();
|
||||
|
||||
let extracted = extract_kernel(&rvf_path).unwrap();
|
||||
assert!(extracted.kernel_path.exists());
|
||||
|
||||
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
|
||||
assert_eq!(on_disk, image);
|
||||
assert_eq!(extracted.header.api_port, 8080);
|
||||
assert_eq!(extracted.cmdline, "console=ttyS0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_kernel_no_cmdline() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let rvf_path = dir.path().join("no_cmd.rvf");
|
||||
|
||||
let opts = RvfOptions {
|
||||
dimension: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
|
||||
|
||||
let image = b"fake-kernel";
|
||||
store
|
||||
.embed_kernel(KernelArch::X86_64 as u8, 0x01, 0, image, 9090, None)
|
||||
.unwrap();
|
||||
store.close().unwrap();
|
||||
|
||||
let extracted = extract_kernel(&rvf_path).unwrap();
|
||||
let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
|
||||
assert_eq!(on_disk, image);
|
||||
assert!(extracted.cmdline.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_returns_error_when_no_kernel() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let rvf_path = dir.path().join("no_kernel.rvf");
|
||||
|
||||
let opts = RvfOptions {
|
||||
dimension: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let store = RvfStore::create(&rvf_path, opts).unwrap();
|
||||
store.close().unwrap();
|
||||
|
||||
let result = extract_kernel(&rvf_path);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
LaunchError::NoKernelSegment { .. } => {}
|
||||
other => panic!("expected NoKernelSegment, got: {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
717
vendor/ruvector/crates/rvf/rvf-launch/src/lib.rs
vendored
Normal file
717
vendor/ruvector/crates/rvf/rvf-launch/src/lib.rs
vendored
Normal file
@@ -0,0 +1,717 @@
|
||||
//! QEMU microVM launcher for RVF computational containers.
|
||||
//!
|
||||
//! This crate extracts a kernel image from an RVF file's KERNEL_SEG,
|
||||
//! builds a QEMU command line, launches the VM, and provides a handle
|
||||
//! for management (query, shutdown, kill) via QMP.
|
||||
|
||||
pub mod error;
|
||||
pub mod extract;
|
||||
pub mod qemu;
|
||||
pub mod qmp;
|
||||
|
||||
use std::io::Read;
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rvf_types::kernel::KernelArch;
|
||||
|
||||
pub use error::LaunchError;
|
||||
|
||||
/// Configuration for launching an RVF microVM.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LaunchConfig {
|
||||
/// Path to the RVF store file.
|
||||
pub rvf_path: PathBuf,
|
||||
/// Memory allocation in MiB.
|
||||
pub memory_mb: u32,
|
||||
/// Number of virtual CPUs.
|
||||
pub vcpus: u32,
|
||||
/// Host port to forward to the VM's API port (guest :8080).
|
||||
pub api_port: u16,
|
||||
/// Optional host port to forward to the VM's SSH port (guest :2222).
|
||||
pub ssh_port: Option<u16>,
|
||||
/// Whether to enable KVM acceleration (falls back to TCG if unavailable
|
||||
/// unless the kernel requires KVM).
|
||||
pub enable_kvm: bool,
|
||||
/// Override the QEMU binary path.
|
||||
pub qemu_binary: Option<PathBuf>,
|
||||
/// Extra arguments to pass to QEMU.
|
||||
pub extra_args: Vec<String>,
|
||||
/// Override the kernel image path (skip extraction from RVF).
|
||||
pub kernel_path: Option<PathBuf>,
|
||||
/// Override the initramfs path.
|
||||
pub initramfs_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for LaunchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rvf_path: PathBuf::new(),
|
||||
memory_mb: 128,
|
||||
vcpus: 1,
|
||||
api_port: 8080,
|
||||
ssh_port: None,
|
||||
enable_kvm: true,
|
||||
qemu_binary: None,
|
||||
extra_args: Vec::new(),
|
||||
kernel_path: None,
|
||||
initramfs_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current status of the microVM.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum VmStatus {
|
||||
/// QEMU process is running.
|
||||
Running,
|
||||
/// QEMU process has exited.
|
||||
Exited(Option<i32>),
|
||||
}
|
||||
|
||||
/// A running QEMU microVM.
|
||||
pub struct MicroVm {
|
||||
process: Child,
|
||||
api_port: u16,
|
||||
ssh_port: Option<u16>,
|
||||
qmp_socket: PathBuf,
|
||||
pid: u32,
|
||||
/// Holds the extracted kernel temp files alive.
|
||||
_extracted: Option<extract::ExtractedKernel>,
|
||||
/// Holds the work directory alive.
|
||||
_workdir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
/// Result of a requirements check.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequirementsReport {
|
||||
/// Whether qemu-system-x86_64 (or arch equivalent) was found.
|
||||
pub qemu_found: bool,
|
||||
/// Path to the QEMU binary, if found.
|
||||
pub qemu_path: Option<PathBuf>,
|
||||
/// Whether KVM acceleration is available.
|
||||
pub kvm_available: bool,
|
||||
/// Platform-specific install instructions if QEMU is missing.
|
||||
pub install_hint: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequirementsReport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.qemu_found {
|
||||
writeln!(
|
||||
f,
|
||||
"QEMU: found at {}",
|
||||
self.qemu_path.as_ref().unwrap().display()
|
||||
)?;
|
||||
} else {
|
||||
writeln!(f, "QEMU: NOT FOUND")?;
|
||||
writeln!(f, " Install instructions:")?;
|
||||
writeln!(f, " {}", self.install_hint)?;
|
||||
}
|
||||
writeln!(
|
||||
f,
|
||||
"KVM: {}",
|
||||
if self.kvm_available {
|
||||
"available"
|
||||
} else {
|
||||
"not available (will use TCG)"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of what a launch would execute, without spawning QEMU.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DryRunResult {
|
||||
/// The full QEMU command line that would be executed.
|
||||
pub command_line: Vec<String>,
|
||||
/// Path to the kernel image that would be used.
|
||||
pub kernel_path: PathBuf,
|
||||
/// Path to the initramfs, if any.
|
||||
pub initramfs_path: Option<PathBuf>,
|
||||
/// The kernel command line that would be passed.
|
||||
pub cmdline: String,
|
||||
/// Whether KVM would be used.
|
||||
pub use_kvm: bool,
|
||||
/// Memory allocation in MiB.
|
||||
pub memory_mb: u32,
|
||||
/// Number of virtual CPUs.
|
||||
pub vcpus: u32,
|
||||
/// The API port mapping.
|
||||
pub api_port: u16,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DryRunResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Dry run - QEMU command that would be executed:")?;
|
||||
writeln!(f, " {}", self.command_line.join(" "))?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, " Kernel: {}", self.kernel_path.display())?;
|
||||
if let Some(ref initrd) = self.initramfs_path {
|
||||
writeln!(f, " Initramfs: {}", initrd.display())?;
|
||||
}
|
||||
writeln!(f, " Cmdline: {}", self.cmdline)?;
|
||||
writeln!(
|
||||
f,
|
||||
" KVM: {}",
|
||||
if self.use_kvm { "yes" } else { "no (TCG)" }
|
||||
)?;
|
||||
writeln!(f, " Memory: {} MiB", self.memory_mb)?;
|
||||
writeln!(f, " vCPUs: {}", self.vcpus)?;
|
||||
writeln!(f, " API port: {}", self.api_port)
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level launcher API.
|
||||
pub struct Launcher;
|
||||
|
||||
impl Launcher {
|
||||
/// Check whether all requirements for launching a microVM are met.
|
||||
///
|
||||
/// Returns a `RequirementsReport` with details about what was found
|
||||
/// and platform-specific install instructions if QEMU is missing.
|
||||
pub fn check_requirements(arch: KernelArch) -> RequirementsReport {
|
||||
let qemu_result = qemu::find_qemu(arch);
|
||||
let kvm = qemu::kvm_available();
|
||||
|
||||
let install_hint = match std::env::consts::OS {
|
||||
"linux" => {
|
||||
// Detect package manager
|
||||
if std::path::Path::new("/usr/bin/apt").exists()
|
||||
|| std::path::Path::new("/usr/bin/apt-get").exists()
|
||||
{
|
||||
"sudo apt install qemu-system-x86".to_string()
|
||||
} else if std::path::Path::new("/usr/bin/dnf").exists() {
|
||||
"sudo dnf install qemu-system-x86".to_string()
|
||||
} else if std::path::Path::new("/usr/bin/pacman").exists() {
|
||||
"sudo pacman -S qemu-system-x86".to_string()
|
||||
} else if std::path::Path::new("/sbin/apk").exists() {
|
||||
"sudo apk add qemu-system-x86_64".to_string()
|
||||
} else {
|
||||
"Install QEMU via your distribution's package manager \
|
||||
(e.g. apt, dnf, pacman)"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
"macos" => "brew install qemu".to_string(),
|
||||
_ => "Download QEMU from https://www.qemu.org/download/".to_string(),
|
||||
};
|
||||
|
||||
match qemu_result {
|
||||
Ok(path) => RequirementsReport {
|
||||
qemu_found: true,
|
||||
qemu_path: Some(path),
|
||||
kvm_available: kvm,
|
||||
install_hint,
|
||||
},
|
||||
Err(_) => RequirementsReport {
|
||||
qemu_found: false,
|
||||
qemu_path: None,
|
||||
kvm_available: kvm,
|
||||
install_hint,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract kernel from an RVF file and launch it in a QEMU microVM.
|
||||
///
|
||||
/// Calls `check_requirements()` first and returns a helpful error if
|
||||
/// QEMU is not found.
|
||||
pub fn launch(config: &LaunchConfig) -> Result<MicroVm, LaunchError> {
|
||||
if !config.rvf_path.exists() {
|
||||
return Err(LaunchError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("RVF file not found: {}", config.rvf_path.display()),
|
||||
)));
|
||||
}
|
||||
|
||||
// Check requirements first (unless user provided a custom binary)
|
||||
if config.qemu_binary.is_none() {
|
||||
let report = Self::check_requirements(KernelArch::X86_64);
|
||||
if !report.qemu_found {
|
||||
return Err(LaunchError::QemuNotFound {
|
||||
searched: vec![format!(
|
||||
"QEMU not found. Install it with: {}",
|
||||
report.install_hint,
|
||||
)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract kernel from RVF
|
||||
let extracted = extract::extract_kernel(&config.rvf_path)?;
|
||||
|
||||
// Create a working directory for QMP socket, logs, etc.
|
||||
let workdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
|
||||
|
||||
// Build the QEMU command
|
||||
let qemu_cmd = qemu::build_command(config, &extracted, workdir.path())?;
|
||||
|
||||
let qmp_socket = qemu_cmd.qmp_socket.clone();
|
||||
|
||||
// Spawn QEMU
|
||||
let mut command = qemu_cmd.command;
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let child = command.spawn().map_err(LaunchError::QemuSpawn)?;
|
||||
|
||||
let pid = child.id();
|
||||
|
||||
Ok(MicroVm {
|
||||
process: child,
|
||||
api_port: config.api_port,
|
||||
ssh_port: config.ssh_port,
|
||||
qmp_socket,
|
||||
pid,
|
||||
_extracted: Some(extracted),
|
||||
_workdir: workdir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Show what WOULD be executed without actually spawning QEMU.
|
||||
///
|
||||
/// Useful for CI/testing and debugging launch configuration. Extracts
|
||||
/// the kernel from the RVF file and builds the full command line, but
|
||||
/// does not spawn any process.
|
||||
pub fn dry_run(config: &LaunchConfig) -> Result<DryRunResult, LaunchError> {
|
||||
if !config.rvf_path.exists() {
|
||||
return Err(LaunchError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("RVF file not found: {}", config.rvf_path.display()),
|
||||
)));
|
||||
}
|
||||
|
||||
let extracted = extract::extract_kernel(&config.rvf_path)?;
|
||||
let workdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;
|
||||
let qemu_cmd = qemu::build_command(config, &extracted, workdir.path())?;
|
||||
|
||||
// Reconstruct the command line as a Vec<String>
|
||||
let cmd = &qemu_cmd.command;
|
||||
let program = cmd.get_program().to_string_lossy().to_string();
|
||||
let args: Vec<String> = cmd
|
||||
.get_args()
|
||||
.map(|a| a.to_string_lossy().to_string())
|
||||
.collect();
|
||||
let mut command_line = vec![program];
|
||||
command_line.extend(args);
|
||||
|
||||
let kernel_path = config
|
||||
.kernel_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| extracted.kernel_path.clone());
|
||||
|
||||
let initramfs_path = config
|
||||
.initramfs_path
|
||||
.clone()
|
||||
.or_else(|| extracted.initramfs_path.clone());
|
||||
|
||||
let use_kvm = config.enable_kvm && qemu::kvm_available();
|
||||
|
||||
Ok(DryRunResult {
|
||||
command_line,
|
||||
kernel_path,
|
||||
initramfs_path,
|
||||
cmdline: extracted.cmdline,
|
||||
use_kvm,
|
||||
memory_mb: config.memory_mb,
|
||||
vcpus: config.vcpus,
|
||||
api_port: config.api_port,
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the QEMU binary for the given architecture.
|
||||
pub fn find_qemu(arch: KernelArch) -> Result<PathBuf, LaunchError> {
|
||||
qemu::find_qemu(arch)
|
||||
}
|
||||
|
||||
/// Check if KVM is available on this host.
|
||||
pub fn kvm_available() -> bool {
|
||||
qemu::kvm_available()
|
||||
}
|
||||
}
|
||||
|
||||
impl MicroVm {
|
||||
/// Wait for the VM's API port to accept TCP connections.
|
||||
pub fn wait_ready(&mut self, timeout: Duration) -> Result<(), LaunchError> {
|
||||
let start = Instant::now();
|
||||
let addr = format!("127.0.0.1:{}", self.api_port);
|
||||
|
||||
loop {
|
||||
// Check if the process has exited
|
||||
if let Some(exit) = self.try_wait_process()? {
|
||||
let mut stderr_buf = String::new();
|
||||
if let Some(ref mut stderr) = self.process.stderr {
|
||||
let _ = stderr.read_to_string(&mut stderr_buf);
|
||||
}
|
||||
return Err(LaunchError::QemuExited {
|
||||
code: exit,
|
||||
stderr: stderr_buf,
|
||||
});
|
||||
}
|
||||
|
||||
// Try connecting to the API port
|
||||
if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(200))
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if start.elapsed() >= timeout {
|
||||
return Err(LaunchError::Timeout {
|
||||
seconds: timeout.as_secs(),
|
||||
});
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a vector query to the running VM's HTTP API.
|
||||
pub fn query(
|
||||
&self,
|
||||
vector: &[f32],
|
||||
k: usize,
|
||||
) -> Result<Vec<rvf_runtime::SearchResult>, LaunchError> {
|
||||
let _url = format!("http://127.0.0.1:{}/query", self.api_port);
|
||||
|
||||
// Build JSON payload
|
||||
let payload = serde_json::json!({
|
||||
"vector": vector,
|
||||
"k": k,
|
||||
});
|
||||
let body =
|
||||
serde_json::to_vec(&payload).map_err(|e| LaunchError::Io(std::io::Error::other(e)))?;
|
||||
|
||||
// Use a raw TCP connection to send an HTTP POST (avoids depending
|
||||
// on a full HTTP client library).
|
||||
let addr = format!("127.0.0.1:{}", self.api_port);
|
||||
let mut stream = TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(5))
|
||||
.map_err(LaunchError::Io)?;
|
||||
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(30)))
|
||||
.map_err(LaunchError::Io)?;
|
||||
|
||||
use std::io::Write;
|
||||
let request = format!(
|
||||
"POST /query HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
Content-Length: {}\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n",
|
||||
self.api_port,
|
||||
body.len(),
|
||||
);
|
||||
stream
|
||||
.write_all(request.as_bytes())
|
||||
.map_err(LaunchError::Io)?;
|
||||
stream.write_all(&body).map_err(LaunchError::Io)?;
|
||||
|
||||
let mut response = String::new();
|
||||
stream
|
||||
.read_to_string(&mut response)
|
||||
.map_err(LaunchError::Io)?;
|
||||
|
||||
// Parse the HTTP response body (skip headers)
|
||||
let body_start = response.find("\r\n\r\n").map(|i| i + 4).unwrap_or(0);
|
||||
let resp_body = &response[body_start..];
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct QueryResult {
|
||||
id: u64,
|
||||
distance: f32,
|
||||
}
|
||||
|
||||
let results: Vec<QueryResult> = serde_json::from_str(resp_body).map_err(|e| {
|
||||
LaunchError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
})?;
|
||||
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| rvf_runtime::SearchResult {
|
||||
id: r.id,
|
||||
distance: r.distance,
|
||||
retrieval_quality: rvf_types::quality::RetrievalQuality::Full,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the current VM status.
|
||||
pub fn status(&mut self) -> VmStatus {
|
||||
match self.process.try_wait() {
|
||||
Ok(Some(status)) => VmStatus::Exited(status.code()),
|
||||
Ok(None) => VmStatus::Running,
|
||||
Err(_) => VmStatus::Exited(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Graceful shutdown: try QMP `system_powerdown`, fall back to SIGTERM.
|
||||
pub fn shutdown(&mut self) -> Result<(), LaunchError> {
|
||||
// Try QMP first
|
||||
if self.qmp_socket.exists() {
|
||||
match qmp::QmpClient::connect(&self.qmp_socket, Duration::from_secs(5)) {
|
||||
Ok(mut client) => {
|
||||
let _ = client.system_powerdown();
|
||||
|
||||
// Wait up to 10 seconds for the VM to shut down
|
||||
let start = Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(10) {
|
||||
if let Ok(Some(_)) = self.process.try_wait() {
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
// Still running, try quit
|
||||
let _ = client.quit();
|
||||
let start = Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(5) {
|
||||
if let Ok(Some(_)) = self.process.try_wait() {
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// QMP not available, fall through to SIGTERM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to SIGTERM (via kill on Unix)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unsafe {
|
||||
libc_kill(self.pid as i32);
|
||||
}
|
||||
let start = Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(5) {
|
||||
if let Ok(Some(_)) = self.process.try_wait() {
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: kill -9
|
||||
let _ = self.process.kill();
|
||||
let _ = self.process.wait();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Force-kill the VM process immediately.
|
||||
pub fn kill(&mut self) -> Result<(), LaunchError> {
|
||||
self.process.kill().map_err(LaunchError::Io)?;
|
||||
let _ = self.process.wait();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the QEMU process PID.
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Get the API port.
|
||||
pub fn api_port(&self) -> u16 {
|
||||
self.api_port
|
||||
}
|
||||
|
||||
/// Get the SSH port, if configured.
|
||||
pub fn ssh_port(&self) -> Option<u16> {
|
||||
self.ssh_port
|
||||
}
|
||||
|
||||
/// Get the QMP socket path.
|
||||
pub fn qmp_socket(&self) -> &PathBuf {
|
||||
&self.qmp_socket
|
||||
}
|
||||
|
||||
fn try_wait_process(&mut self) -> Result<Option<Option<i32>>, LaunchError> {
|
||||
match self.process.try_wait() {
|
||||
Ok(Some(status)) => Ok(Some(status.code())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(LaunchError::Io(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MicroVm {
|
||||
fn drop(&mut self) {
|
||||
// Best-effort cleanup: try to kill the process if still running.
|
||||
if let Ok(None) = self.process.try_wait() {
|
||||
let _ = self.process.kill();
|
||||
let _ = self.process.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send SIGTERM on Unix. Avoids a libc dependency by using a raw syscall.
|
||||
#[cfg(unix)]
|
||||
unsafe fn libc_kill(pid: i32) {
|
||||
// SIGTERM = 15 on all Unix platforms
|
||||
// We use std::process::Command as a portable way to send signals.
|
||||
let _ = std::process::Command::new("kill")
|
||||
.args(["-TERM", &pid.to_string()])
|
||||
.output();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_config() {
|
||||
let config = LaunchConfig::default();
|
||||
assert_eq!(config.memory_mb, 128);
|
||||
assert_eq!(config.vcpus, 1);
|
||||
assert_eq!(config.api_port, 8080);
|
||||
assert!(config.enable_kvm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_status_variants() {
|
||||
assert_eq!(VmStatus::Running, VmStatus::Running);
|
||||
assert_eq!(VmStatus::Exited(Some(0)), VmStatus::Exited(Some(0)));
|
||||
assert_ne!(VmStatus::Running, VmStatus::Exited(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_requirements_returns_report() {
|
||||
let report = Launcher::check_requirements(KernelArch::X86_64);
|
||||
// Install hint should never be empty
|
||||
assert!(!report.install_hint.is_empty());
|
||||
// Display formatting should work
|
||||
let display = format!("{report}");
|
||||
assert!(display.contains("QEMU:"));
|
||||
assert!(display.contains("KVM:"));
|
||||
|
||||
if report.qemu_found {
|
||||
assert!(report.qemu_path.is_some());
|
||||
} else {
|
||||
assert!(report.qemu_path.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_requirements_has_platform_install_hint() {
|
||||
let report = Launcher::check_requirements(KernelArch::X86_64);
|
||||
// On Linux CI we expect an apt/dnf/pacman hint
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert!(
|
||||
report.install_hint.contains("apt")
|
||||
|| report.install_hint.contains("dnf")
|
||||
|| report.install_hint.contains("pacman")
|
||||
|| report.install_hint.contains("apk")
|
||||
|| report.install_hint.contains("package manager"),
|
||||
"expected Linux install hint, got: {}",
|
||||
report.install_hint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_rejects_missing_rvf() {
|
||||
let config = LaunchConfig {
|
||||
rvf_path: PathBuf::from("/nonexistent/test.rvf"),
|
||||
..Default::default()
|
||||
};
|
||||
let result = Launcher::launch(&config);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_rejects_missing_rvf() {
|
||||
let config = LaunchConfig {
|
||||
rvf_path: PathBuf::from("/nonexistent/test.rvf"),
|
||||
..Default::default()
|
||||
};
|
||||
let result = Launcher::dry_run(&config);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_with_real_rvf() {
|
||||
use rvf_runtime::options::RvfOptions;
|
||||
use rvf_runtime::RvfStore;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let rvf_path = dir.path().join("dry_run.rvf");
|
||||
|
||||
let opts = RvfOptions {
|
||||
dimension: 4,
|
||||
..Default::default()
|
||||
};
|
||||
let mut store = RvfStore::create(&rvf_path, opts).unwrap();
|
||||
let image = b"MZ\x00fake-kernel-for-dry-run-test";
|
||||
store
|
||||
.embed_kernel(
|
||||
KernelArch::X86_64 as u8,
|
||||
0x01,
|
||||
0,
|
||||
image,
|
||||
8080,
|
||||
Some("console=ttyS0"),
|
||||
)
|
||||
.unwrap();
|
||||
store.close().unwrap();
|
||||
|
||||
let config = LaunchConfig {
|
||||
rvf_path: rvf_path.clone(),
|
||||
memory_mb: 256,
|
||||
vcpus: 2,
|
||||
api_port: 9090,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = Launcher::dry_run(&config);
|
||||
// dry_run may fail if QEMU binary not found - that is expected
|
||||
match result {
|
||||
Ok(dry) => {
|
||||
assert!(!dry.command_line.is_empty());
|
||||
assert!(dry.command_line[0].contains("qemu"));
|
||||
assert_eq!(dry.memory_mb, 256);
|
||||
assert_eq!(dry.vcpus, 2);
|
||||
assert_eq!(dry.api_port, 9090);
|
||||
assert_eq!(dry.cmdline, "console=ttyS0");
|
||||
// Display should work
|
||||
let display = format!("{dry}");
|
||||
assert!(display.contains("Dry run"));
|
||||
assert!(display.contains("256 MiB"));
|
||||
}
|
||||
Err(LaunchError::QemuNotFound { .. }) => {
|
||||
// Expected in environments without QEMU
|
||||
}
|
||||
Err(other) => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_report_display() {
|
||||
let report = RequirementsReport {
|
||||
qemu_found: true,
|
||||
qemu_path: Some(PathBuf::from("/usr/bin/qemu-system-x86_64")),
|
||||
kvm_available: false,
|
||||
install_hint: "sudo apt install qemu-system-x86".to_string(),
|
||||
};
|
||||
let s = format!("{report}");
|
||||
assert!(s.contains("/usr/bin/qemu-system-x86_64"));
|
||||
assert!(s.contains("not available"));
|
||||
|
||||
let report_missing = RequirementsReport {
|
||||
qemu_found: false,
|
||||
qemu_path: None,
|
||||
kvm_available: false,
|
||||
install_hint: "brew install qemu".to_string(),
|
||||
};
|
||||
let s2 = format!("{report_missing}");
|
||||
assert!(s2.contains("NOT FOUND"));
|
||||
assert!(s2.contains("brew install qemu"));
|
||||
}
|
||||
}
|
||||
241
vendor/ruvector/crates/rvf/rvf-launch/src/qemu.rs
vendored
Normal file
241
vendor/ruvector/crates/rvf/rvf-launch/src/qemu.rs
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
//! QEMU command-line builder.
|
||||
//!
|
||||
//! Constructs the `qemu-system-*` command line for launching a microVM
|
||||
//! from an extracted RVF kernel.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use rvf_types::kernel::{KernelArch, KERNEL_FLAG_REQUIRES_KVM};
|
||||
|
||||
use crate::error::LaunchError;
|
||||
use crate::extract::ExtractedKernel;
|
||||
use crate::LaunchConfig;
|
||||
|
||||
/// Resolved QEMU invocation ready to be spawned.
|
||||
pub struct QemuCommand {
|
||||
pub command: Command,
|
||||
pub qmp_socket: PathBuf,
|
||||
}
|
||||
|
||||
/// Check if KVM is available on this host.
|
||||
pub fn kvm_available() -> bool {
|
||||
Path::new("/dev/kvm").exists()
|
||||
&& std::fs::metadata("/dev/kvm")
|
||||
.map(|m| {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = m.permissions().mode();
|
||||
// Check if the file is readable+writable by someone
|
||||
mode & 0o666 != 0
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Locate the QEMU binary for the given architecture.
|
||||
pub fn find_qemu(arch: KernelArch) -> Result<PathBuf, LaunchError> {
|
||||
let candidates = match arch {
|
||||
KernelArch::X86_64 | KernelArch::Universal | KernelArch::Unknown => {
|
||||
vec![
|
||||
"qemu-system-x86_64",
|
||||
"/usr/bin/qemu-system-x86_64",
|
||||
"/usr/local/bin/qemu-system-x86_64",
|
||||
]
|
||||
}
|
||||
KernelArch::Aarch64 => {
|
||||
vec![
|
||||
"qemu-system-aarch64",
|
||||
"/usr/bin/qemu-system-aarch64",
|
||||
"/usr/local/bin/qemu-system-aarch64",
|
||||
]
|
||||
}
|
||||
KernelArch::Riscv64 => {
|
||||
vec![
|
||||
"qemu-system-riscv64",
|
||||
"/usr/bin/qemu-system-riscv64",
|
||||
"/usr/local/bin/qemu-system-riscv64",
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
for candidate in &candidates {
|
||||
if let Ok(output) = std::process::Command::new("which").arg(candidate).output() {
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
// Also check if the path exists directly (absolute paths)
|
||||
let p = Path::new(candidate);
|
||||
if p.is_absolute() && p.exists() {
|
||||
return Ok(p.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
Err(LaunchError::QemuNotFound {
|
||||
searched: candidates.iter().map(|s| s.to_string()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a QEMU command for the given config and extracted kernel.
|
||||
pub fn build_command(
|
||||
config: &LaunchConfig,
|
||||
extracted: &ExtractedKernel,
|
||||
work_dir: &Path,
|
||||
) -> Result<QemuCommand, LaunchError> {
|
||||
let arch = KernelArch::try_from(extracted.header.arch).unwrap_or(KernelArch::X86_64);
|
||||
|
||||
// Resolve QEMU binary
|
||||
let qemu_bin = match &config.qemu_binary {
|
||||
Some(p) => {
|
||||
if !p.exists() {
|
||||
return Err(LaunchError::QemuNotFound {
|
||||
searched: vec![p.display().to_string()],
|
||||
});
|
||||
}
|
||||
p.clone()
|
||||
}
|
||||
None => find_qemu(arch)?,
|
||||
};
|
||||
|
||||
// KVM
|
||||
let use_kvm = if config.enable_kvm {
|
||||
if kvm_available() {
|
||||
true
|
||||
} else if extracted.header.kernel_flags & KERNEL_FLAG_REQUIRES_KVM != 0 {
|
||||
return Err(LaunchError::KvmRequired);
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let qmp_socket = work_dir.join("qmp.sock");
|
||||
|
||||
let mut cmd = Command::new(&qemu_bin);
|
||||
|
||||
// Machine type
|
||||
match arch {
|
||||
KernelArch::X86_64 | KernelArch::Universal | KernelArch::Unknown => {
|
||||
if use_kvm {
|
||||
cmd.args(["-machine", "microvm,accel=kvm"]);
|
||||
cmd.args(["-cpu", "host"]);
|
||||
} else {
|
||||
cmd.args(["-machine", "microvm,accel=tcg"]);
|
||||
cmd.args(["-cpu", "qemu64"]);
|
||||
}
|
||||
}
|
||||
KernelArch::Aarch64 => {
|
||||
if use_kvm {
|
||||
cmd.args(["-machine", "virt,accel=kvm"]);
|
||||
cmd.args(["-cpu", "host"]);
|
||||
} else {
|
||||
cmd.args(["-machine", "virt,accel=tcg"]);
|
||||
cmd.args(["-cpu", "cortex-a72"]);
|
||||
}
|
||||
}
|
||||
KernelArch::Riscv64 => {
|
||||
if use_kvm {
|
||||
cmd.args(["-machine", "virt,accel=kvm"]);
|
||||
cmd.args(["-cpu", "host"]);
|
||||
} else {
|
||||
cmd.args(["-machine", "virt,accel=tcg"]);
|
||||
cmd.args(["-cpu", "rv64"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memory and CPUs
|
||||
cmd.arg("-m").arg(format!("{}M", config.memory_mb));
|
||||
cmd.arg("-smp").arg(config.vcpus.to_string());
|
||||
|
||||
// Kernel image
|
||||
let kernel_path = config
|
||||
.kernel_path
|
||||
.as_deref()
|
||||
.unwrap_or(&extracted.kernel_path);
|
||||
cmd.arg("-kernel").arg(kernel_path);
|
||||
|
||||
// Initramfs
|
||||
let initramfs = config
|
||||
.initramfs_path
|
||||
.as_deref()
|
||||
.or(extracted.initramfs_path.as_deref());
|
||||
if let Some(initrd) = initramfs {
|
||||
cmd.arg("-initrd").arg(initrd);
|
||||
}
|
||||
|
||||
// Kernel command line
|
||||
let default_cmdline = format!(
|
||||
"console=ttyS0 reboot=t panic=-1 rvf.port={}",
|
||||
config.api_port
|
||||
);
|
||||
let cmdline = if extracted.cmdline.is_empty() {
|
||||
default_cmdline
|
||||
} else {
|
||||
format!("{} {}", extracted.cmdline, default_cmdline)
|
||||
};
|
||||
cmd.arg("-append").arg(&cmdline);
|
||||
|
||||
// RVF file as a virtio-blk device (read-only)
|
||||
cmd.arg("-drive").arg(format!(
|
||||
"id=rvf,file={},format=raw,if=none,readonly=on",
|
||||
config.rvf_path.display()
|
||||
));
|
||||
cmd.args(["-device", "virtio-blk-device,drive=rvf"]);
|
||||
|
||||
// Network: forward API port and optional SSH port
|
||||
let mut hostfwd = format!("user,id=net0,hostfwd=tcp::{}:-:8080", config.api_port);
|
||||
if let Some(ssh_port) = config.ssh_port {
|
||||
hostfwd.push_str(&format!(",hostfwd=tcp::{}:-:2222", ssh_port));
|
||||
}
|
||||
cmd.arg("-netdev").arg(&hostfwd);
|
||||
cmd.args(["-device", "virtio-net-device,netdev=net0"]);
|
||||
|
||||
// Serial console on stdio
|
||||
cmd.args(["-chardev", "stdio,id=char0"]);
|
||||
cmd.args(["-serial", "chardev:char0"]);
|
||||
|
||||
// QMP socket for management
|
||||
cmd.arg("-qmp")
|
||||
.arg(format!("unix:{},server,nowait", qmp_socket.display()));
|
||||
|
||||
// No graphics, no reboot on panic
|
||||
cmd.arg("-nographic");
|
||||
cmd.arg("-no-reboot");
|
||||
|
||||
// Extra user-specified arguments
|
||||
for arg in &config.extra_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(QemuCommand {
|
||||
command: cmd,
|
||||
qmp_socket,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kvm_detection_does_not_panic() {
|
||||
// Just ensure the function runs without panicking.
|
||||
let _ = kvm_available();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_qemu_returns_result() {
|
||||
// In CI, QEMU may not be installed, so we just check it returns
|
||||
// either Ok or a proper error.
|
||||
let result = find_qemu(KernelArch::X86_64);
|
||||
match result {
|
||||
Ok(path) => assert!(path.to_str().unwrap().contains("qemu")),
|
||||
Err(LaunchError::QemuNotFound { searched }) => {
|
||||
assert!(!searched.is_empty());
|
||||
}
|
||||
Err(other) => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
106
vendor/ruvector/crates/rvf/rvf-launch/src/qmp.rs
vendored
Normal file
106
vendor/ruvector/crates/rvf/rvf-launch/src/qmp.rs
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
//! QMP (QEMU Machine Protocol) client.
|
||||
//!
|
||||
//! Implements just enough of the QMP JSON protocol to negotiate
|
||||
//! capabilities and issue `system_powerdown` / `quit` commands for
|
||||
//! graceful or forced VM shutdown.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::LaunchError;
|
||||
|
||||
/// A minimal QMP client connected via a Unix socket.
|
||||
pub struct QmpClient {
|
||||
stream: UnixStream,
|
||||
}
|
||||
|
||||
impl QmpClient {
|
||||
/// Connect to the QMP Unix socket and perform the capability
|
||||
/// negotiation handshake.
|
||||
pub fn connect(socket_path: &Path, timeout: Duration) -> Result<Self, LaunchError> {
|
||||
let stream = UnixStream::connect(socket_path).map_err(LaunchError::QmpIo)?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(LaunchError::QmpIo)?;
|
||||
stream
|
||||
.set_write_timeout(Some(timeout))
|
||||
.map_err(LaunchError::QmpIo)?;
|
||||
|
||||
let mut client = Self { stream };
|
||||
|
||||
// Read the server greeting (QMP banner).
|
||||
let greeting = client.read_line()?;
|
||||
if !greeting.contains("\"QMP\"") {
|
||||
return Err(LaunchError::Qmp(format!(
|
||||
"unexpected QMP greeting: {greeting}"
|
||||
)));
|
||||
}
|
||||
|
||||
// Negotiate capabilities.
|
||||
client.send_command(r#"{"execute":"qmp_capabilities"}"#)?;
|
||||
let resp = client.read_line()?;
|
||||
if !resp.contains("\"return\"") {
|
||||
return Err(LaunchError::Qmp(format!("qmp_capabilities failed: {resp}")));
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Send `system_powerdown` for a graceful ACPI shutdown.
|
||||
pub fn system_powerdown(&mut self) -> Result<(), LaunchError> {
|
||||
self.send_command(r#"{"execute":"system_powerdown"}"#)?;
|
||||
let resp = self.read_line()?;
|
||||
if resp.contains("\"error\"") {
|
||||
return Err(LaunchError::Qmp(format!("system_powerdown failed: {resp}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send `quit` to force QEMU to exit immediately.
|
||||
pub fn quit(&mut self) -> Result<(), LaunchError> {
|
||||
self.send_command(r#"{"execute":"quit"}"#)?;
|
||||
// QEMU may close the socket before we can read the response.
|
||||
let _ = self.read_line();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send `query-status` to check the VM's run state.
|
||||
pub fn query_status(&mut self) -> Result<String, LaunchError> {
|
||||
self.send_command(r#"{"execute":"query-status"}"#)?;
|
||||
self.read_line()
|
||||
}
|
||||
|
||||
fn send_command(&mut self, cmd: &str) -> Result<(), LaunchError> {
|
||||
self.stream
|
||||
.write_all(cmd.as_bytes())
|
||||
.map_err(LaunchError::QmpIo)?;
|
||||
self.stream.write_all(b"\n").map_err(LaunchError::QmpIo)?;
|
||||
self.stream.flush().map_err(LaunchError::QmpIo)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Result<String, LaunchError> {
|
||||
let mut reader = BufReader::new(&self.stream);
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).map_err(LaunchError::QmpIo)?;
|
||||
Ok(line)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// QMP tests require a running QEMU instance, so we only test
|
||||
// construction logic here. Full integration tests belong in
|
||||
// tests/rvf-integration.
|
||||
#[test]
|
||||
fn connect_to_nonexistent_socket_fails() {
|
||||
use super::*;
|
||||
let result = QmpClient::connect(
|
||||
Path::new("/tmp/nonexistent_qmp.sock"),
|
||||
Duration::from_secs(1),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user