use crate::cloudhv::config::{DeviceConfig, VMConfig};
use crate::cloudhv::errors::{CloudHypervisorError, Result};
use hyperlocal::{UnixClientExt, Uri};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Clone)]
pub struct APIClient {
socket_path: PathBuf,
client: hyper::Client<hyperlocal::UnixConnector>,
}
impl APIClient {
pub fn new(socket: impl Into<PathBuf>) -> Result<Self> {
let socket_path = socket.into();
if !socket_path.exists() {
return Err(CloudHypervisorError::Api(format!(
"API socket not found: {}",
socket_path.display()
)));
}
let client = hyper::Client::unix();
Ok(Self {
socket_path,
client,
})
}
pub async fn vm_create(&self, config: VMConfig) -> Result<String> {
let payload = serde_json::to_string(&config)?;
let response = self.put_request("/api/v1/vm.create", &payload).await?;
if response.status().is_success() {
Ok(uuid::Uuid::new_v4().to_string())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to create VM: {}",
error_msg
)))
}
}
pub async fn vm_boot(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.boot", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to boot VM: {}",
error_msg
)))
}
}
pub async fn vm_shutdown(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.shutdown", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to shutdown VM: {}",
error_msg
)))
}
}
pub async fn vm_power_button(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.power-button", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to trigger power button: {}",
error_msg
)))
}
}
pub async fn vm_pause(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.pause", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to pause VM: {}",
error_msg
)))
}
}
pub async fn vm_resume(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.resume", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to resume VM: {}",
error_msg
)))
}
}
pub async fn vm_info(&self) -> Result<VMInfo> {
let response = self.get_request("/api/v1/vm.info").await?;
if response.status().is_success() {
let body = hyper::body::to_bytes(response.into_body()).await?;
let vm_info: VMInfo = serde_json::from_slice(&body)?;
Ok(vm_info)
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to get VM info: {}",
error_msg
)))
}
}
pub async fn vm_delete(&self) -> Result<()> {
let response = self.put_request("/api/v1/vm.delete", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to delete VM: {}",
error_msg
)))
}
}
pub async fn vm_resize_memory(&self, size: u64) -> Result<()> {
let payload = serde_json::json!({
"desired_ram": size
});
let response = self
.put_request("/api/v1/vm.resize", &payload.to_string())
.await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to resize VM memory: {}",
error_msg
)))
}
}
pub async fn vm_resize_vcpu(&self, desired_vcpus: u32) -> Result<()> {
let payload = serde_json::json!({
"desired_vcpus": desired_vcpus
});
let response = self
.put_request("/api/v1/vm.resize", &payload.to_string())
.await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to resize VM vCPUs: {}",
error_msg
)))
}
}
pub async fn vm_resize_zone(&self, zone_id: &str, size: u64) -> Result<()> {
let payload = serde_json::json!({
"id": zone_id,
"desired_ram": size
});
let response = self
.put_request("/api/v1/vm.resize-zone", &payload.to_string())
.await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to resize zone {}: {}",
zone_id, error_msg
)))
}
}
pub async fn vm_add_device(&self, device: DeviceConfig) -> Result<String> {
let endpoint = match device.device_type {
crate::cloudhv::config::DeviceType::Disk(_) => "/api/v1/vm.add-disk",
crate::cloudhv::config::DeviceType::Net(_) => "/api/v1/vm.add-net",
crate::cloudhv::config::DeviceType::Fs(_) => "/api/v1/vm.add-fs",
crate::cloudhv::config::DeviceType::VhostUserNet(_) => "/api/v1/vm.add-net",
crate::cloudhv::config::DeviceType::VhostUserBlk(_) => "/api/v1/vm.add-disk",
crate::cloudhv::config::DeviceType::Vfio(_) => {
return Err(CloudHypervisorError::NotSupported {
feature: "VFIO hot-plug".to_string(),
reason: "VFIO devices must be configured at VM creation time. Runtime hot-plug is not supported due to PCI topology and IOMMU constraints.".to_string(),
});
}
};
let payload = serde_json::to_string(&device)?;
let response = self.put_request(endpoint, &payload).await?;
if response.status().is_success() {
let body = hyper::body::to_bytes(response.into_body()).await?;
let result: serde_json::Value = serde_json::from_slice(&body)?;
Ok(result["id"].as_str().unwrap_or(&device.id).to_string())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to add device: {}",
error_msg
)))
}
}
pub async fn vm_remove_device(&self, device_id: &str) -> Result<()> {
let payload = serde_json::json!({
"id": device_id
});
let response = self
.put_request("/api/v1/vm.remove-device", &payload.to_string())
.await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to remove device {}: {}",
device_id, error_msg
)))
}
}
pub async fn vm_snapshot(&self, destination_url: &str) -> Result<()> {
let payload = serde_json::json!({
"destination_url": destination_url
});
let response = self
.put_request("/api/v1/vm.snapshot", &payload.to_string())
.await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to snapshot VM: {}",
error_msg
)))
}
}
pub async fn vm_restore(&self, restore_config: RestoreConfig) -> Result<()> {
let payload = serde_json::to_string(&restore_config)?;
let response = self.put_request("/api/v1/vm.restore", &payload).await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to restore VM: {}",
error_msg
)))
}
}
pub async fn vmm_ping(&self) -> Result<VmmPingResponse> {
let response = self.get_request("/api/v1/vmm.ping").await?;
if response.status().is_success() {
let body = hyper::body::to_bytes(response.into_body()).await?;
let ping_response: VmmPingResponse = serde_json::from_slice(&body)?;
Ok(ping_response)
} else {
Err(CloudHypervisorError::Api(
"Failed to ping VMM".to_string(),
))
}
}
pub async fn vmm_shutdown(&self) -> Result<()> {
let response = self.put_request("/api/v1/vmm.shutdown", "").await?;
if response.status().is_success() {
Ok(())
} else {
let body = hyper::body::to_bytes(response.into_body()).await?;
let error_msg = String::from_utf8_lossy(&body);
Err(CloudHypervisorError::Api(format!(
"Failed to shutdown VMM: {}",
error_msg
)))
}
}
async fn get_request(&self, path: &str) -> Result<hyper::Response<hyper::Body>> {
let url = Uri::new(&self.socket_path, path);
let request = hyper::Request::builder()
.method(hyper::Method::GET)
.uri(url)
.body(hyper::Body::empty())
.map_err(|e| CloudHypervisorError::Api(format!("Failed to build request: {}", e)))?;
self.client
.request(request)
.await
.map_err(|e| CloudHypervisorError::Api(format!("Request failed: {}", e)))
}
async fn put_request(
&self,
path: &str,
body: &str,
) -> Result<hyper::Response<hyper::Body>> {
let url = Uri::new(&self.socket_path, path);
let request = hyper::Request::builder()
.method(hyper::Method::PUT)
.uri(url)
.header("Content-Type", "application/json")
.body(hyper::Body::from(body.to_string()))
.map_err(|e| CloudHypervisorError::Api(format!("Failed to build request: {}", e)))?;
self.client
.request(request)
.await
.map_err(|e| CloudHypervisorError::Api(format!("Request failed: {}", e)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VMInfo {
pub config: serde_json::Value,
pub state: String,
#[serde(default)]
pub memory_actual_size: Option<u64>,
#[serde(default)]
pub device_tree: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreConfig {
pub source_url: String,
pub prefault: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmmPingResponse {
pub version: String,
}