//! HTTP client for machines service (SSH operations). use std::time::Duration; use reqwest::Client; use serde::{Deserialize, Serialize}; use crate::error::{ServiceError, ServiceResult}; /// Client for calling machines service (SSH operations) #[derive(Debug, Clone)] pub struct MachinesClient { base_url: String, http_client: Client, timeout: Duration, } /// Command execution request #[derive(Debug, Serialize, Deserialize)] pub struct ExecuteCommandRequest { /// Target machine name or hostname pub host: String, /// Command to execute pub command: String, /// Optional retry policy #[serde(skip_serializing_if = "Option::is_none")] pub retry_policy: Option, } /// Retry policy for command execution #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RetryPolicy { /// Maximum number of retries pub max_retries: u32, /// Backoff strategy: "exponential" or "linear" pub backoff: String, /// Initial backoff duration in milliseconds pub initial_backoff_ms: u64, } /// Command execution response #[derive(Debug, Serialize, Deserialize)] pub struct ExecuteCommandResponse { /// Command output (stdout) pub output: String, /// Error output (stderr) if any #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, /// Process exit code pub exit_code: i32, /// Execution time in milliseconds pub execution_time_ms: u64, } /// Machine pool status #[derive(Debug, Serialize, Deserialize)] pub struct MachinePoolStatus { /// Number of active connections pub active_connections: usize, /// Number of pending operations pub pending_operations: usize, /// Health status: "healthy", "degraded", "unhealthy" pub health: String, /// Timestamp of last update pub last_updated: String, } /// Machine configuration info #[derive(Debug, Serialize, Deserialize)] pub struct MachineInfo { /// Machine name/identifier pub name: String, /// SSH address pub address: String, /// SSH port pub port: u16, /// Authentication method pub auth_method: String, } impl MachinesClient { /// Create a new machines service client pub fn new(base_url: impl Into) -> ServiceResult { let url = base_url.into(); // Validate URL format if !url.starts_with("http://") && !url.starts_with("https://") { return Err(ServiceError::InvalidUrl(url)); } Ok(Self { base_url: url, http_client: Client::new(), timeout: Duration::from_secs(30), }) } /// Create a new machines service client with custom timeout pub fn with_timeout(base_url: impl Into, timeout: Duration) -> ServiceResult { let mut client = Self::new(base_url)?; client.timeout = timeout; Ok(client) } /// Execute a command on a remote machine via SSH pub async fn execute_command( &self, host: impl Into, command: impl Into, ) -> ServiceResult { let request = ExecuteCommandRequest { host: host.into(), command: command.into(), retry_policy: None, }; self.execute_command_with_retry(request).await } /// Execute a command with retry policy pub async fn execute_command_with_retry( &self, request: ExecuteCommandRequest, ) -> ServiceResult { let url = format!("{}/api/v1/machines/execute", self.base_url); let response = self .http_client .post(&url) .timeout(self.timeout) .json(&request) .send() .await?; if !response.status().is_success() { let status = response.status().as_u16(); let body = response.text().await.unwrap_or_default(); return Err(ServiceError::from_status(status, body)); } Ok(response.json().await?) } /// Get machine pool status pub async fn pool_status(&self) -> ServiceResult { let url = format!("{}/api/v1/machines/pool/status", self.base_url); let response = self .http_client .get(&url) .timeout(self.timeout) .send() .await?; if !response.status().is_success() { let status = response.status().as_u16(); let body = response.text().await.unwrap_or_default(); return Err(ServiceError::from_status(status, body)); } Ok(response.json().await?) } /// Get information about a specific machine pub async fn get_machine(&self, name: impl Into) -> ServiceResult { let name = name.into(); let url = format!("{}/api/v1/machines/{}", self.base_url, name); let response = self .http_client .get(&url) .timeout(self.timeout) .send() .await?; match response.status().as_u16() { 404 => Err(ServiceError::MachineNotFound(name)), status if !response.status().is_success() => { let body = response.text().await.unwrap_or_default(); Err(ServiceError::from_status(status, body)) } _ => Ok(response.json().await?), } } /// List all machines pub async fn list_machines(&self) -> ServiceResult> { let url = format!("{}/api/v1/machines", self.base_url); let response = self .http_client .get(&url) .timeout(self.timeout) .send() .await?; if !response.status().is_success() { let status = response.status().as_u16(); let body = response.text().await.unwrap_or_default(); return Err(ServiceError::from_status(status, body)); } Ok(response.json().await?) } /// Health check for machines service pub async fn health_check(&self) -> ServiceResult { let url = format!("{}/health", self.base_url); match self .http_client .get(&url) .timeout(Duration::from_secs(5)) .send() .await { Ok(response) => Ok(response.status().is_success()), Err(_) => Err(ServiceError::ServiceUnavailable( "Machines service unavailable".to_string(), )), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_machines_client_creation() { let client = MachinesClient::new("http://localhost:8081"); assert!(client.is_ok()); } #[test] fn test_invalid_url() { let client = MachinesClient::new("invalid-url"); assert!(client.is_err()); } #[test] fn test_execute_command_request() { let request = ExecuteCommandRequest { host: "server1".to_string(), command: "ls -la".to_string(), retry_policy: None, }; let json = serde_json::to_string(&request).unwrap(); assert!(json.contains("server1")); assert!(json.contains("ls -la")); } }