herolib-virt 0.3.13

Virtualization and container management for herolib (buildah, nerdctl, kubernetes)
Documentation
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() {
            // Cloud Hypervisor returns 204 No Content on successful VM creation
            // Generate a UUID for the VM since the API doesn't return one
            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,
}