- Created T026-practical-test task.yaml for MVP smoke testing - Added k8shost-server to flake.nix (packages, apps, overlays) - Staged all workspace directories for nix flake build - Updated flake.nix shellHook to include k8shost Resolves: T026.S1 blocker (R8 - nix submodule visibility)
254 lines
7.4 KiB
Rust
254 lines
7.4 KiB
Rust
//! FireCracker REST API client over Unix socket
|
|
|
|
use plasmavmc_types::{Error, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::Path;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::UnixStream;
|
|
|
|
/// FireCracker REST API client
|
|
pub struct FireCrackerClient {
|
|
stream: UnixStream,
|
|
}
|
|
|
|
impl FireCrackerClient {
|
|
/// Connect to FireCracker API socket
|
|
pub async fn connect(path: impl AsRef<Path>) -> Result<Self> {
|
|
let stream = UnixStream::connect(path.as_ref())
|
|
.await
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to connect FireCracker API: {e}")))?;
|
|
Ok(Self { stream })
|
|
}
|
|
|
|
/// Send HTTP request over Unix socket
|
|
async fn request(
|
|
&mut self,
|
|
method: &str,
|
|
path: &str,
|
|
body: Option<&[u8]>,
|
|
) -> Result<Vec<u8>> {
|
|
let mut request = format!("{} {} HTTP/1.1\r\n", method, path);
|
|
request.push_str("Host: localhost\r\n");
|
|
request.push_str("Content-Type: application/json\r\n");
|
|
|
|
if let Some(body) = body {
|
|
request.push_str(&format!("Content-Length: {}\r\n", body.len()));
|
|
} else {
|
|
request.push_str("Content-Length: 0\r\n");
|
|
}
|
|
request.push_str("\r\n");
|
|
|
|
if let Some(body) = body {
|
|
request.push_str(&String::from_utf8_lossy(body));
|
|
}
|
|
|
|
self.stream
|
|
.write_all(request.as_bytes())
|
|
.await
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to send request: {e}")))?;
|
|
self.stream
|
|
.flush()
|
|
.await
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to flush request: {e}")))?;
|
|
|
|
// Read response
|
|
let mut response = Vec::new();
|
|
self.stream
|
|
.read_to_end(&mut response)
|
|
.await
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to read response: {e}")))?;
|
|
|
|
// Parse HTTP response
|
|
let response_str = String::from_utf8_lossy(&response);
|
|
let mut lines = response_str.lines();
|
|
|
|
// Parse status line
|
|
let status_line = lines.next().ok_or_else(|| {
|
|
Error::HypervisorError("Empty response from FireCracker API".to_string())
|
|
})?;
|
|
if !status_line.contains("200") && !status_line.contains("204") {
|
|
return Err(Error::HypervisorError(format!(
|
|
"FireCracker API error: {status_line}"
|
|
)));
|
|
}
|
|
|
|
// Skip headers
|
|
let mut body_start = 0;
|
|
for (idx, line) in response_str.lines().enumerate() {
|
|
if line.is_empty() {
|
|
body_start = response_str
|
|
.lines()
|
|
.take(idx + 1)
|
|
.map(|l| l.len() + 2) // +2 for \r\n
|
|
.sum();
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(response[body_start..].to_vec())
|
|
}
|
|
|
|
/// PUT /machine-config
|
|
pub async fn put_machine_config(&mut self, vcpu_count: u32, mem_size_mib: u64) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct MachineConfig {
|
|
vcpu_count: u32,
|
|
mem_size_mib: u64,
|
|
ht_enabled: bool,
|
|
}
|
|
|
|
let config = MachineConfig {
|
|
vcpu_count,
|
|
mem_size_mib,
|
|
ht_enabled: false,
|
|
};
|
|
|
|
let body = serde_json::to_vec(&config)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize config: {e}")))?;
|
|
|
|
self.request("PUT", "/machine-config", Some(&body))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PUT /boot-source
|
|
pub async fn put_boot_source(
|
|
&mut self,
|
|
kernel_image_path: &str,
|
|
initrd_path: Option<&str>,
|
|
boot_args: &str,
|
|
) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct BootSource {
|
|
kernel_image_path: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
initrd_path: Option<String>,
|
|
boot_args: String,
|
|
}
|
|
|
|
let boot_source = BootSource {
|
|
kernel_image_path: kernel_image_path.to_string(),
|
|
initrd_path: initrd_path.map(|s| s.to_string()),
|
|
boot_args: boot_args.to_string(),
|
|
};
|
|
|
|
let body = serde_json::to_vec(&boot_source)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize boot source: {e}")))?;
|
|
|
|
self.request("PUT", "/boot-source", Some(&body))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PUT /drives/{drive_id}
|
|
pub async fn put_drive(
|
|
&mut self,
|
|
drive_id: &str,
|
|
path_on_host: &str,
|
|
is_root_device: bool,
|
|
is_read_only: bool,
|
|
) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct Drive {
|
|
path_on_host: String,
|
|
is_root_device: bool,
|
|
is_read_only: bool,
|
|
}
|
|
|
|
let drive = Drive {
|
|
path_on_host: path_on_host.to_string(),
|
|
is_root_device,
|
|
is_read_only,
|
|
};
|
|
|
|
let body = serde_json::to_vec(&drive)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize drive: {e}")))?;
|
|
|
|
self.request("PUT", &format!("/drives/{}", drive_id), Some(&body))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PUT /network-interfaces/{iface_id}
|
|
pub async fn put_network_interface(
|
|
&mut self,
|
|
iface_id: &str,
|
|
guest_mac: &str,
|
|
host_dev_name: &str,
|
|
) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct NetworkInterface {
|
|
guest_mac: String,
|
|
host_dev_name: String,
|
|
}
|
|
|
|
let iface = NetworkInterface {
|
|
guest_mac: guest_mac.to_string(),
|
|
host_dev_name: host_dev_name.to_string(),
|
|
};
|
|
|
|
let body = serde_json::to_vec(&iface)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize network interface: {e}")))?;
|
|
|
|
self.request(
|
|
"PUT",
|
|
&format!("/network-interfaces/{}", iface_id),
|
|
Some(&body),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PUT /actions
|
|
pub async fn instance_start(&mut self) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct Action {
|
|
action_type: String,
|
|
}
|
|
|
|
let action = Action {
|
|
action_type: "InstanceStart".to_string(),
|
|
};
|
|
|
|
let body = serde_json::to_vec(&action)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize action: {e}")))?;
|
|
|
|
self.request("PUT", "/actions", Some(&body))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PUT /actions (SendCtrlAltDel)
|
|
pub async fn send_ctrl_alt_del(&mut self) -> Result<()> {
|
|
#[derive(Serialize)]
|
|
struct Action {
|
|
action_type: String,
|
|
}
|
|
|
|
let action = Action {
|
|
action_type: "SendCtrlAltDel".to_string(),
|
|
};
|
|
|
|
let body = serde_json::to_vec(&action)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to serialize action: {e}")))?;
|
|
|
|
self.request("PUT", "/actions", Some(&body))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// GET /vm
|
|
pub async fn get_vm_info(&mut self) -> Result<VmInfo> {
|
|
let body = self.request("GET", "/vm", None).await?;
|
|
let info: VmInfo = serde_json::from_slice(&body)
|
|
.map_err(|e| Error::HypervisorError(format!("Failed to parse VM info: {e}")))?;
|
|
Ok(info)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VmInfo {
|
|
#[serde(default)]
|
|
#[allow(dead_code)]
|
|
pub state: String,
|
|
}
|