Platform restructured into crates/, added AI service and detector,
migrated control-center-ui to Leptos 0.8
252 lines
7.2 KiB
Rust
252 lines
7.2 KiB
Rust
//! 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<RetryPolicy>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
/// 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<String>) -> ServiceResult<Self> {
|
|
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<String>, timeout: Duration) -> ServiceResult<Self> {
|
|
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<String>,
|
|
command: impl Into<String>,
|
|
) -> ServiceResult<ExecuteCommandResponse> {
|
|
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<ExecuteCommandResponse> {
|
|
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<MachinePoolStatus> {
|
|
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<String>) -> ServiceResult<MachineInfo> {
|
|
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<Vec<MachineInfo>> {
|
|
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<bool> {
|
|
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"));
|
|
}
|
|
}
|