New crates
- platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
subject prefixing under provisioning.>, 6 stream definitions on startup
- platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL
Service integrations
- orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
- control-center: solo_auth_middleware (intentional bypass, --mode solo only),
NATS session events, WebSocket bridge via JetStream subscription (no polling)
- vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
SurrealDB storage backend with MVCC retry + exponential backoff
- secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
- extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
cache invalidation via provisioning.workspace.*.deploy.done
Clippy workspace clean
cargo clippy --workspace -- -D warnings: 0 errors
Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
(let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
353 lines
11 KiB
Rust
353 lines
11 KiB
Rust
#![allow(
|
|
dead_code,
|
|
unused_imports,
|
|
unused_variables,
|
|
unused_assignments,
|
|
unused,
|
|
clippy::excessive_nesting
|
|
)]
|
|
|
|
//! Core provisioning engine for executing infrastructure commands
|
|
|
|
use std::process::Stdio;
|
|
|
|
use anyhow::{Context, Result};
|
|
use serde_json::Value;
|
|
use tracing::{debug, info};
|
|
|
|
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 = "provisioning"; // Use default command name
|
|
|
|
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))?;
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|