313 lines
11 KiB
Rust
313 lines
11 KiB
Rust
|
|
//! Core provisioning engine for executing infrastructure commands
|
||
|
|
|
||
|
|
use anyhow::{Context, Result};
|
||
|
|
use serde_json::Value;
|
||
|
|
use std::process::{Command, Stdio};
|
||
|
|
use tokio::process::Command as AsyncCommand;
|
||
|
|
use tracing::{info, debug, warn};
|
||
|
|
|
||
|
|
use crate::config::Config;
|
||
|
|
use crate::errors::ProvisioningError;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct ServerConfig {
|
||
|
|
pub hostname: String,
|
||
|
|
pub instance_type: String,
|
||
|
|
pub count: u32,
|
||
|
|
pub provider: String,
|
||
|
|
pub region: Option<String>,
|
||
|
|
pub purpose: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub struct ProvisioningEngine {
|
||
|
|
config: Config,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ProvisioningEngine {
|
||
|
|
pub fn new(config: &Config) -> Result<Self> {
|
||
|
|
Ok(Self {
|
||
|
|
config: config.clone(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create servers based on parsed configuration
|
||
|
|
pub fn create_server(&self, server_config: &ServerConfig, check_mode: bool) -> Result<String> {
|
||
|
|
info!("Creating server: {:?}", server_config);
|
||
|
|
|
||
|
|
let mut args = vec![
|
||
|
|
"server".to_string(),
|
||
|
|
"create".to_string(),
|
||
|
|
"--infra".to_string(),
|
||
|
|
format!("ai-{}", server_config.hostname),
|
||
|
|
];
|
||
|
|
|
||
|
|
if check_mode {
|
||
|
|
args.push("--check".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(region) = &server_config.region {
|
||
|
|
args.extend(vec!["--region".to_string(), region.clone()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
args.push("--out".to_string());
|
||
|
|
args.push("json".to_string());
|
||
|
|
|
||
|
|
let result = self.execute_provisioning_command(&args)?;
|
||
|
|
|
||
|
|
// Parse and format the result
|
||
|
|
if let Ok(json_result) = serde_json::from_str::<Value>(&result) {
|
||
|
|
Ok(self.format_server_result(&json_result, check_mode))
|
||
|
|
} else {
|
||
|
|
Ok(result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Process natural language infrastructure queries
|
||
|
|
pub fn process_query(&self, query: &str, infra_name: Option<&str>, output_format: &str) -> Result<String> {
|
||
|
|
info!("Processing query: {}", query);
|
||
|
|
|
||
|
|
let mut args = vec!["show".to_string()];
|
||
|
|
|
||
|
|
// Determine what to show based on the query
|
||
|
|
if query.to_lowercase().contains("server") || query.to_lowercase().contains("instance") {
|
||
|
|
args.push("servers".to_string());
|
||
|
|
} else if query.to_lowercase().contains("cost") || query.to_lowercase().contains("price") {
|
||
|
|
args.push("costs".to_string());
|
||
|
|
} else if query.to_lowercase().contains("status") {
|
||
|
|
args.push("data".to_string());
|
||
|
|
} else {
|
||
|
|
args.push("settings".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(infra) = infra_name {
|
||
|
|
args.extend(vec!["--infra".to_string(), infra.to_string()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
args.extend(vec!["--out".to_string(), output_format.to_string()]);
|
||
|
|
|
||
|
|
let result = self.execute_provisioning_command(&args)?;
|
||
|
|
|
||
|
|
// Add natural language interpretation
|
||
|
|
Ok(format!(
|
||
|
|
"Based on your query: \"{}\"\n\n{}",
|
||
|
|
query,
|
||
|
|
self.interpret_query_result(query, &result)
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Deploy a task service
|
||
|
|
pub fn deploy_taskserv(&self, service_name: &str, infra_name: &str, configuration: &Value, check_mode: bool) -> Result<String> {
|
||
|
|
info!("Deploying service {} to {}", service_name, infra_name);
|
||
|
|
|
||
|
|
let mut args = vec![
|
||
|
|
"taskserv".to_string(),
|
||
|
|
"create".to_string(),
|
||
|
|
service_name.to_string(),
|
||
|
|
"--infra".to_string(),
|
||
|
|
infra_name.to_string(),
|
||
|
|
];
|
||
|
|
|
||
|
|
if check_mode {
|
||
|
|
args.push("--check".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
args.extend(vec!["--out".to_string(), "json".to_string()]);
|
||
|
|
|
||
|
|
let result = self.execute_provisioning_command(&args)?;
|
||
|
|
|
||
|
|
Ok(format!(
|
||
|
|
"🚀 Service '{}' deployment on '{}' infrastructure:\n\n{}",
|
||
|
|
service_name,
|
||
|
|
infra_name,
|
||
|
|
result
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a complete cluster
|
||
|
|
pub fn create_cluster(&self, description: &str, cluster_type: &str, services: &[&str], infra_name: &str) -> Result<String> {
|
||
|
|
info!("Creating {} cluster: {}", cluster_type, description);
|
||
|
|
|
||
|
|
// First create the infrastructure
|
||
|
|
let mut results = Vec::new();
|
||
|
|
|
||
|
|
// Generate infrastructure
|
||
|
|
let gen_args = vec![
|
||
|
|
"generate".to_string(),
|
||
|
|
"infra".to_string(),
|
||
|
|
"--new".to_string(),
|
||
|
|
infra_name.to_string(),
|
||
|
|
"--template".to_string(),
|
||
|
|
format!("{}-cluster", cluster_type),
|
||
|
|
];
|
||
|
|
|
||
|
|
match self.execute_provisioning_command(&gen_args) {
|
||
|
|
Ok(result) => results.push(format!("📋 Infrastructure generated:\n{}", result)),
|
||
|
|
Err(e) => results.push(format!("⚠️ Infrastructure generation: {}", e)),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create servers
|
||
|
|
let server_args = vec![
|
||
|
|
"server".to_string(),
|
||
|
|
"create".to_string(),
|
||
|
|
"--infra".to_string(),
|
||
|
|
infra_name.to_string(),
|
||
|
|
];
|
||
|
|
|
||
|
|
match self.execute_provisioning_command(&server_args) {
|
||
|
|
Ok(result) => results.push(format!("🖥️ Servers created:\n{}", result)),
|
||
|
|
Err(e) => results.push(format!("⚠️ Server creation: {}", e)),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deploy each service
|
||
|
|
for service in services {
|
||
|
|
let service_args = vec![
|
||
|
|
"taskserv".to_string(),
|
||
|
|
"create".to_string(),
|
||
|
|
service.to_string(),
|
||
|
|
"--infra".to_string(),
|
||
|
|
infra_name.to_string(),
|
||
|
|
];
|
||
|
|
|
||
|
|
match self.execute_provisioning_command(&service_args) {
|
||
|
|
Ok(result) => results.push(format!("⚙️ Service '{}' deployed:\n{}", service, result)),
|
||
|
|
Err(e) => results.push(format!("⚠️ Service '{}': {}", service, e)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(results.join("\n\n"))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get status of infrastructure
|
||
|
|
pub fn get_status(&self, infra_name: Option<&str>, detailed: bool) -> Result<String> {
|
||
|
|
let mut args = vec!["show".to_string()];
|
||
|
|
|
||
|
|
if detailed {
|
||
|
|
args.push("alldata".to_string());
|
||
|
|
} else {
|
||
|
|
args.push("data".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(infra) = infra_name {
|
||
|
|
args.extend(vec!["--infra".to_string(), infra.to_string()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
args.extend(vec!["--out".to_string(), "json".to_string()]);
|
||
|
|
|
||
|
|
let result = self.execute_provisioning_command(&args)?;
|
||
|
|
|
||
|
|
// Parse and format the status
|
||
|
|
if let Ok(json_result) = serde_json::from_str::<Value>(&result) {
|
||
|
|
Ok(self.format_status_result(&json_result, detailed))
|
||
|
|
} else {
|
||
|
|
Ok(result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Execute a provisioning command
|
||
|
|
fn execute_provisioning_command(&self, args: &[String]) -> Result<String> {
|
||
|
|
debug!("Executing command: {:?}", args);
|
||
|
|
|
||
|
|
let cmd_path = self.config.provisioning_command();
|
||
|
|
|
||
|
|
let output = std::process::Command::new(&cmd_path)
|
||
|
|
.args(args)
|
||
|
|
.stdout(Stdio::piped())
|
||
|
|
.stderr(Stdio::piped())
|
||
|
|
.output()
|
||
|
|
.with_context(|| format!("Failed to execute command: {}", cmd_path.display()))?;
|
||
|
|
|
||
|
|
if !output.status.success() {
|
||
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
|
|
return Err(ProvisioningError::command_failed(format!(
|
||
|
|
"Command failed with status {}: {}",
|
||
|
|
output.status.code().unwrap_or(-1),
|
||
|
|
stderr
|
||
|
|
)).into());
|
||
|
|
}
|
||
|
|
|
||
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
|
|
Ok(stdout.to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Format server creation result
|
||
|
|
fn format_server_result(&self, result: &Value, check_mode: bool) -> String {
|
||
|
|
let prefix = if check_mode {
|
||
|
|
"🔍 Check mode result"
|
||
|
|
} else {
|
||
|
|
"✅ Server creation result"
|
||
|
|
};
|
||
|
|
|
||
|
|
if let Some(servers) = result.get("servers").and_then(|s| s.as_array()) {
|
||
|
|
let mut output = format!("{}\n\n", prefix);
|
||
|
|
|
||
|
|
for server in servers {
|
||
|
|
if let (Some(hostname), Some(status)) =
|
||
|
|
(server.get("hostname").and_then(|h| h.as_str()),
|
||
|
|
server.get("status").and_then(|s| s.as_str())) {
|
||
|
|
|
||
|
|
output.push_str(&format!("• {}: {}\n", hostname, status));
|
||
|
|
|
||
|
|
if let Some(ip) = server.get("public_ip").and_then(|ip| ip.as_str()) {
|
||
|
|
output.push_str(&format!(" IP: {}\n", ip));
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(cost) = server.get("cost_hour").and_then(|c| c.as_str()) {
|
||
|
|
output.push_str(&format!(" Cost: {}/hour\n", cost));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
output
|
||
|
|
} else {
|
||
|
|
format!("{}\n\n{}", prefix, serde_json::to_string_pretty(result).unwrap_or_default())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Format status result
|
||
|
|
fn format_status_result(&self, result: &Value, detailed: bool) -> String {
|
||
|
|
let mut output = String::new();
|
||
|
|
|
||
|
|
if let Some(main_name) = result.get("main_name").and_then(|n| n.as_str()) {
|
||
|
|
output.push_str(&format!("📊 Infrastructure: {}\n\n", main_name));
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(servers) = result.get("servers").and_then(|s| s.as_array()) {
|
||
|
|
output.push_str("🖥️ Servers:\n");
|
||
|
|
for server in servers {
|
||
|
|
if let Some(hostname) = server.get("hostname").and_then(|h| h.as_str()) {
|
||
|
|
let status = server.get("status").and_then(|s| s.as_str()).unwrap_or("unknown");
|
||
|
|
let emoji = match status {
|
||
|
|
"running" => "🟢",
|
||
|
|
"stopped" => "🔴",
|
||
|
|
"pending" => "🟡",
|
||
|
|
_ => "⚪",
|
||
|
|
};
|
||
|
|
output.push_str(&format!(" {} {}: {}\n", emoji, hostname, status));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
output.push('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
if detailed {
|
||
|
|
if let Some(costs) = result.get("costs") {
|
||
|
|
output.push_str(&format!("💰 Costs: {}\n", serde_json::to_string_pretty(costs).unwrap_or_default()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
output
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Interpret query result with natural language
|
||
|
|
fn interpret_query_result(&self, query: &str, result: &str) -> String {
|
||
|
|
// Simple interpretation based on query keywords
|
||
|
|
if query.to_lowercase().contains("cost") || query.to_lowercase().contains("price") {
|
||
|
|
format!("💰 Cost analysis:\n{}", result)
|
||
|
|
} else if query.to_lowercase().contains("status") || query.to_lowercase().contains("health") {
|
||
|
|
format!("📊 System status:\n{}", result)
|
||
|
|
} else if query.to_lowercase().contains("problem") || query.to_lowercase().contains("error") {
|
||
|
|
format!("🔍 Issue analysis:\n{}", result)
|
||
|
|
} else {
|
||
|
|
result.to_string()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|