prvng_platform/mcp-server/src/provisioning.rs

313 lines
11 KiB
Rust
Raw Normal View History

2025-10-07 10:59:52 +01:00
//! 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()
}
}
}