From aa69c63cee5a1670a75d8029947b79099cbe9bbe Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 19 Oct 2025 11:08:23 -0300 Subject: [PATCH] Refactor bootstrap and package manager, add ureq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split package manager into separate modules - Expose only the installer API - Simplify BootstrapManager to install components and load config - Pin ureq to 3.1.2 and add ureq‑proto crate - Clean up configuration code and remove legacy comments - Update helper scripts and server start command formatting --- Cargo.lock | 33 +- Cargo.toml | 1 + add-req.sh | 1 - fix-errors.sh | 13 +- gbot.sh | 5 +- prompts/dev/platform/fix-errors.md | 23 - prompts/dev/platform/shared.md | 1 + src/bootstrap/mod.rs | 516 +--------- src/config/mod.rs | 50 +- src/main.rs | 108 ++- src/package_manager/mod.rs | 1445 +--------------------------- 11 files changed, 175 insertions(+), 2021 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba716e98f..1512ea03b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,7 +509,7 @@ dependencies = [ "quote", "serde", "serde_json", - "ureq", + "ureq 2.12.1", ] [[package]] @@ -1056,6 +1056,7 @@ dependencies = [ "tokio-stream", "tracing", "tracing-subscriber", + "ureq 3.1.2", "urlencoding", "uuid", "zip 2.4.2", @@ -5658,6 +5659,36 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls 0.23.32", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots 1.0.3", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64 0.22.1", + "http 1.3.1", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.7" diff --git a/Cargo.toml b/Cargo.toml index 219aee32f..48344b6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,3 +92,4 @@ rand = "0.9.2" pdf-extract = "0.10.0" scraper = "0.20" sha2 = "0.10.9" +ureq = "3.1.2" diff --git a/add-req.sh b/add-req.sh index e645e196c..c8949b4ae 100755 --- a/add-req.sh +++ b/add-req.sh @@ -4,7 +4,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$SCRIPT_DIR" OUTPUT_FILE="/tmp/prompt.out" -rm -f "$OUTPUT_FILE" echo "Consolidated LLM Context" > "$OUTPUT_FILE" prompts=( diff --git a/fix-errors.sh b/fix-errors.sh index 13058238c..d82b64390 100755 --- a/fix-errors.sh +++ b/fix-errors.sh @@ -3,13 +3,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$SCRIPT_DIR" OUTPUT_FILE="/tmp/prompt.out" -rm $OUTPUT_FILE + echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE" prompts=( + "./prompts/dev/platform/fix-errors.md" "./prompts/dev/platform/shared.md" "./Cargo.toml" - "./prompts/dev/platform/fix-errors.md" ) for file in "${prompts[@]}"; do @@ -22,17 +22,18 @@ dirs=( #"automation" #"basic" #"bot" - "bootstrap" + #"bootstrap" #"channels" - #"config" + "config" #"context" #"email" #"file" #"llm" #"llm_legacy" #"org" - "session" - "shared" + "package_manager" + #"session" + #"shared" #"tests" #"tools" #"web_automation" diff --git a/gbot.sh b/gbot.sh index 2aaeef89e..5cf36e4a0 100755 --- a/gbot.sh +++ b/gbot.sh @@ -1,2 +1,3 @@ - -clear && cargo build && sudo RUST_BACKTRACE=1 ./target/debug/botserver +clear && \ + cargo build && \ + sudo RUST_BACKTRACE=1 ./target/debug/botserver install tables diff --git a/prompts/dev/platform/fix-errors.md b/prompts/dev/platform/fix-errors.md index d171d3f5b..1362377fe 100644 --- a/prompts/dev/platform/fix-errors.md +++ b/prompts/dev/platform/fix-errors.md @@ -10,26 +10,3 @@ If something, need to be added to a external file, inform it separated. 3. **Respect Cargo.toml** - Check dependencies, editions, and features to avoid compiler errors 4. **Type safety** - Ensure all types match and trait bounds are satisfied 5. **Ownership rules** - Fix borrowing, ownership, and lifetime issues - - -MORE RULES: -- Return only the modified files as a single `.sh` script using `cat`, so the - code can be restored directly. -- You MUST return exactly this example format: -```sh -#!/bin/bash - -# Restore fixed Rust project - -cat > src/.rs << 'EOF' -use std::io; - -// test - -cat > src/.rs << 'EOF' -// Fixed library code -pub fn add(a: i32, b: i32) -> i32 { - a + b -} -EOF - ----- diff --git a/prompts/dev/platform/shared.md b/prompts/dev/platform/shared.md index a4c9c5fc1..41a462bf5 100644 --- a/prompts/dev/platform/shared.md +++ b/prompts/dev/platform/shared.md @@ -2,6 +2,7 @@ MOST IMPORTANT CODE GENERATION RULES: - No placeholders, never comment/uncomment code, no explanations, no filler text. - All code must be complete, professional, production-ready, and follow KISS - principles. - NEVER return placeholders of any kind, NEVER comment code, only CONDENSED REAL PRODUCTION GRADE code. +- REMOTE ALL COMMENTS FROM GENERATED CODE. DO NOT COMMENT AT ALL, NO TALK! - NEVER say that I have already some part of the code, give me it full again, and working. - Always increment logging with (all-in-one-line) info!, debug!, trace! to give birth to the console. - If the output is too large, split it into multiple parts, but always - include the full updated code files. diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index 53266f4b9..f1c178162 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -1,500 +1,58 @@ use crate::config::AppConfig; use crate::package_manager::{InstallMode, PackageManager}; -use anyhow::{Context, Result}; -use diesel::prelude::*; -use log::{info, warn}; -use std::collections::HashMap; -use std::net::TcpListener; -use std::path::PathBuf; -use std::process::Command; -use std::thread; -use std::time::Duration; +use anyhow::Result; +use log::{debug, info, trace, warn}; pub struct BootstrapManager { - mode: InstallMode, - tenant: String, - base_path: PathBuf, - config_values: HashMap, + pub install_mode: InstallMode, + pub tenant: Option, } impl BootstrapManager { - pub fn new(mode: InstallMode, tenant: Option) -> Self { - let tenant = tenant.unwrap_or_else(|| "default".to_string()); - let base_path = if mode == InstallMode::Container { - PathBuf::from("/opt/gbo") - } else { - PathBuf::from("./botserver-stack") - }; - + pub fn new(install_mode: InstallMode, tenant: Option) -> Self { + info!( + "Initializing BootstrapManager with mode {:?} and tenant {:?}", + install_mode, tenant + ); Self { - mode, + install_mode, tenant, - base_path, - config_values: HashMap::new(), } } pub fn bootstrap(&mut self) -> Result { - info!( - "Starting bootstrap process in {:?} mode for tenant {}", - self.mode, self.tenant - ); + info!("Starting bootstrap process"); - std::fs::create_dir_all(&self.base_path).context("Failed to create base directory")?; + let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; - let pm = PackageManager::new(self.mode.clone(), Some(self.tenant.clone()))?; + let required_components = vec!["drive", "cache", "tables", "llm"]; - info!("Installing core infrastructure components"); - self.install_and_configure_tables(&pm)?; - self.install_and_configure_drive(&pm)?; - self.install_and_configure_cache(&pm)?; - self.install_and_configure_llm(&pm)?; - - info!("Creating database schema and storing configuration"); - let config = self.build_config()?; - self.initialize_database(&config)?; - self.store_configuration_in_db(&config)?; + for component in required_components { + if !pm.is_installed(component) { + info!("Installing required component: {}", component); + futures::executor::block_on(pm.install(component))?; + trace!("Successfully installed component: {}", component); + } else { + debug!("Component {} already installed", component); + } + } info!("Bootstrap completed successfully"); + + let config = match diesel::Connection::establish( + "postgres://botserver:botserver@localhost:5432/botserver", + ) { + Ok(mut conn) => { + trace!("Connected to database for config loading"); + AppConfig::from_database(&mut conn) + } + Err(e) => { + warn!("Failed to connect to database for config: {}", e); + trace!("Falling back to environment configuration"); + AppConfig::from_env() + } + }; + Ok(config) } - - fn install_and_configure_tables(&mut self, pm: &PackageManager) -> Result<()> { - info!("Installing PostgreSQL tables component"); - pm.install("tables")?; - - let tables_port = self.find_available_port(5432); - let tables_password = self.generate_password(); - - self.config_values - .insert("TABLES_USERNAME".to_string(), self.tenant.clone()); - self.config_values - .insert("TABLES_PASSWORD".to_string(), tables_password.clone()); - self.config_values - .insert("TABLES_SERVER".to_string(), self.get_service_host("tables")); - self.config_values - .insert("TABLES_PORT".to_string(), tables_port.to_string()); - self.config_values - .insert("TABLES_DATABASE".to_string(), format!("{}_db", self.tenant)); - - self.wait_for_service(&self.get_service_host("tables"), tables_port, 30)?; - - info!( - "PostgreSQL configured: {}:{}", - self.get_service_host("tables"), - tables_port - ); - Ok(()) - } - - fn install_and_configure_drive(&mut self, pm: &PackageManager) -> Result<()> { - info!("Installing MinIO drive component"); - pm.install("drive")?; - - let drive_port = self.find_available_port(9000); - let _drive_console_port = self.find_available_port(9001); - let drive_user = "minioadmin".to_string(); - let drive_password = self.generate_password(); - - self.config_values.insert( - "DRIVE_SERVER".to_string(), - format!("{}:{}", self.get_service_host("drive"), drive_port), - ); - self.config_values - .insert("DRIVE_ACCESSKEY".to_string(), drive_user.clone()); - self.config_values - .insert("DRIVE_SECRET".to_string(), drive_password.clone()); - self.config_values - .insert("DRIVE_USE_SSL".to_string(), "false".to_string()); - self.config_values - .insert("DRIVE_ORG_PREFIX".to_string(), self.tenant.clone()); - self.config_values.insert( - "DRIVE_BUCKET".to_string(), - format!("{}default.gbai", self.tenant), - ); - - self.wait_for_service(&self.get_service_host("drive"), drive_port, 30)?; - - info!( - "MinIO configured: {}:{}", - self.get_service_host("drive"), - drive_port - ); - Ok(()) - } - - fn install_and_configure_cache(&mut self, pm: &PackageManager) -> Result<()> { - info!("Installing Redis cache component"); - pm.install("cache")?; - - let cache_port = self.find_available_port(6379); - - self.config_values.insert( - "CACHE_URL".to_string(), - format!("redis://{}:{}/", self.get_service_host("cache"), cache_port), - ); - - self.wait_for_service(&self.get_service_host("cache"), cache_port, 30)?; - - info!( - "Redis configured: {}:{}", - self.get_service_host("cache"), - cache_port - ); - Ok(()) - } - - fn install_and_configure_llm(&mut self, pm: &PackageManager) -> Result<()> { - info!("Installing LLM server component"); - pm.install("llm")?; - - let llm_port = self.find_available_port(8081); - - self.config_values.insert( - "LLM_URL".to_string(), - format!("http://{}:{}", self.get_service_host("llm"), llm_port), - ); - self.config_values.insert( - "AI_ENDPOINT".to_string(), - format!("http://{}:{}", self.get_service_host("llm"), llm_port), - ); - self.config_values - .insert("AI_KEY".to_string(), "empty".to_string()); - self.config_values - .insert("AI_INSTANCE".to_string(), "llama-local".to_string()); - self.config_values - .insert("AI_VERSION".to_string(), "1.0".to_string()); - - self.wait_for_service(&self.get_service_host("llm"), llm_port, 60)?; - - info!( - "LLM server configured: {}:{}", - self.get_service_host("llm"), - llm_port - ); - Ok(()) - } - - fn build_config(&self) -> Result { - info!("Building application configuration from discovered services"); - - let get_str = |key: &str, default: &str| -> String { - self.config_values - .get(key) - .cloned() - .unwrap_or_else(|| default.to_string()) - }; - - let get_u32 = |key: &str, default: u32| -> u32 { - self.config_values - .get(key) - .and_then(|v| v.parse().ok()) - .unwrap_or(default) - }; - - let get_u16 = |key: &str, default: u16| -> u16 { - self.config_values - .get(key) - .and_then(|v| v.parse().ok()) - .unwrap_or(default) - }; - - let get_bool = |key: &str, default: bool| -> bool { - self.config_values - .get(key) - .map(|v| v.to_lowercase() == "true") - .unwrap_or(default) - }; - - let stack_path = self.base_path.clone(); - - let database = crate::config::DatabaseConfig { - username: get_str("TABLES_USERNAME", "botserver"), - password: get_str("TABLES_PASSWORD", "botserver"), - server: get_str("TABLES_SERVER", "localhost"), - port: get_u32("TABLES_PORT", 5432), - database: get_str("TABLES_DATABASE", "botserver_db"), - }; - - let database_custom = database.clone(); - - let minio = crate::config::DriveConfig { - server: get_str("DRIVE_SERVER", "localhost:9000"), - access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"), - secret_key: get_str("DRIVE_SECRET", "minioadmin"), - use_ssl: get_bool("DRIVE_USE_SSL", false), - org_prefix: get_str("DRIVE_ORG_PREFIX", "botserver"), - }; - - let email = crate::config::EmailConfig { - from: get_str("EMAIL_FROM", "noreply@example.com"), - server: get_str("EMAIL_SERVER", "smtp.example.com"), - port: get_u16("EMAIL_PORT", 587), - username: get_str("EMAIL_USER", "user"), - password: get_str("EMAIL_PASS", "pass"), - }; - - let ai = crate::config::AIConfig { - instance: get_str("AI_INSTANCE", "llama-local"), - key: get_str("AI_KEY", "empty"), - version: get_str("AI_VERSION", "1.0"), - endpoint: get_str("AI_ENDPOINT", "http://localhost:8081"), - }; - - let server_host = if self.mode == InstallMode::Container { - "0.0.0.0".to_string() - } else { - "127.0.0.1".to_string() - }; - - Ok(AppConfig { - minio, - server: crate::config::ServerConfig { - host: server_host, - port: self.find_available_port(8080), - }, - database, - database_custom, - email, - ai, - s3_bucket: get_str("DRIVE_BUCKET", "default.gbai"), - site_path: format!("{}/sites", stack_path.display()), - stack_path, - db_conn: None, - }) - } - - fn initialize_database(&self, config: &AppConfig) -> Result<()> { - use diesel::pg::PgConnection; - - info!("Initializing database schema at {}", config.database_url()); - - // Attempt to establish a PostgreSQL connection with retries. - let mut retries = 5; - let mut conn = loop { - match PgConnection::establish(&config.database_url()) { - Ok(c) => break c, - Err(e) if retries > 0 => { - warn!("Database connection failed, retrying in 2s: {}", e); - thread::sleep(Duration::from_secs(2)); - retries -= 1; - } - Err(e) => { - return Err(anyhow::anyhow!( - "Failed to connect to database after retries: {}", - e - )) - } - } - }; - - // Create the server_configuration table. - diesel::sql_query( - "CREATE TABLE IF NOT EXISTS server_configuration ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, - config_key TEXT NOT NULL UNIQUE, - config_value TEXT NOT NULL, - config_type TEXT NOT NULL DEFAULT 'string', - is_encrypted BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - )", - ) - .execute(&mut conn) - .context("Failed to create server_configuration table")?; - - // Create the bot_configuration table. - diesel::sql_query( - "CREATE TABLE IF NOT EXISTS bot_configuration ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, - bot_id UUID NOT NULL, - config_key TEXT NOT NULL, - config_value TEXT NOT NULL, - config_type TEXT NOT NULL DEFAULT 'string', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(bot_id, config_key) - )", - ) - .execute(&mut conn) - .context("Failed to create bot_configuration table")?; - - // Create the gbot_config_sync table. - diesel::sql_query( - "CREATE TABLE IF NOT EXISTS gbot_config_sync ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, - bot_id UUID NOT NULL UNIQUE, - config_file_path TEXT NOT NULL, - file_hash TEXT NOT NULL, - last_sync_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - sync_count INTEGER NOT NULL DEFAULT 0 - )", - ) - .execute(&mut conn) - .context("Failed to create gbot_config_sync table")?; - - info!("Database schema initialized successfully"); - Ok(()) - } - - fn store_configuration_in_db(&self, config: &AppConfig) -> Result<()> { - use diesel::pg::PgConnection; - - info!("Storing configuration in database"); - - // Establish a PostgreSQL connection explicitly. - let mut conn = PgConnection::establish(&config.database_url()) - .context("Failed to establish database connection for storing configuration")?; - - // Store dynamic configuration values. - for (key, value) in &self.config_values { - diesel::sql_query( - "INSERT INTO server_configuration (config_key, config_value, config_type) - VALUES ($1, $2, 'string') - ON CONFLICT (config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()", - ) - .bind::(key) - .bind::(value) - .execute(&mut conn) - .with_context(|| format!("Failed to store config key: {}", key))?; - } - - // Store static configuration entries. - diesel::sql_query( - "INSERT INTO server_configuration (config_key, config_value, config_type) - VALUES ('SERVER_HOST', $1, 'string') - ON CONFLICT (config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()", - ) - .bind::(&config.server.host) - .execute(&mut conn) - .context("Failed to store SERVER_HOST")?; - - diesel::sql_query( - "INSERT INTO server_configuration (config_key, config_value, config_type) - VALUES ('SERVER_PORT', $1, 'string') - ON CONFLICT (config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()", - ) - .bind::(&config.server.port.to_string()) - .execute(&mut conn) - .context("Failed to store SERVER_PORT")?; - - diesel::sql_query( - "INSERT INTO server_configuration (config_key, config_value, config_type) - VALUES ('STACK_PATH', $1, 'string') - ON CONFLICT (config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()", - ) - .bind::(&config.stack_path.display().to_string()) - .execute(&mut conn) - .context("Failed to store STACK_PATH")?; - - diesel::sql_query( - "INSERT INTO server_configuration (config_key, config_value, config_type) - VALUES ('SITES_ROOT', $1, 'string') - ON CONFLICT (config_key) - DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()", - ) - .bind::(&config.site_path) - .execute(&mut conn) - .context("Failed to store SITES_ROOT")?; - - info!( - "Configuration stored in database successfully with {} entries", - self.config_values.len() + 4 - ); - Ok(()) - } - - fn find_available_port(&self, preferred: u16) -> u16 { - if self.mode == InstallMode::Container { - return preferred; - } - - for port in preferred..preferred + 100 { - if TcpListener::bind(("127.0.0.1", port)).is_ok() { - return port; - } - } - preferred - } - - fn generate_password(&self) -> String { - use rand::Rng; - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let mut rng = rand::rng(); - (0..16) - .map(|_| { - let idx = rng.random_range(0..CHARSET.len()); - CHARSET[idx] as char - }) - .collect() - } - - fn get_service_host(&self, component: &str) -> String { - match self.mode { - InstallMode::Container => { - let container_name = format!("{}-{}", self.tenant, component); - self.get_container_ip(&container_name) - .unwrap_or_else(|_| "127.0.0.1".to_string()) - } - InstallMode::Local => "127.0.0.1".to_string(), - } - } - - fn get_container_ip(&self, container_name: &str) -> Result { - let output = Command::new("lxc") - .args(&["list", container_name, "--format=json"]) - .output()?; - - if !output.status.success() { - return Err(anyhow::anyhow!("Failed to get container info")); - } - - let json: serde_json::Value = serde_json::from_slice(&output.stdout)?; - - if let Some(ip) = json - .get(0) - .and_then(|c| c.get("state")) - .and_then(|s| s.get("network")) - .and_then(|n| n.get("eth0")) - .and_then(|e| e.get("addresses")) - .and_then(|a| a.get(0)) - .and_then(|a| a.get("address")) - .and_then(|a| a.as_str()) - { - Ok(ip.to_string()) - } else { - Err(anyhow::anyhow!("Could not extract container IP")) - } - } - - fn wait_for_service(&self, host: &str, port: u16, timeout_secs: u64) -> Result<()> { - info!( - "Waiting for service at {}:{} (timeout: {}s)", - host, port, timeout_secs - ); - - let start = std::time::Instant::now(); - while start.elapsed().as_secs() < timeout_secs { - match TcpListener::bind((host, port)) { - Ok(_) => { - thread::sleep(Duration::from_secs(1)); - } - Err(_) => { - info!("Service {}:{} is ready", host, port); - return Ok(()); - } - } - thread::sleep(Duration::from_secs(1)); - } - - Err(anyhow::anyhow!( - "Timeout waiting for service at {}:{}", - host, - port - )) - } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 60152d6dc..856162f16 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; -/// Application configuration - reads from database instead of .env #[derive(Clone)] pub struct AppConfig { pub minio: DriveConfig, @@ -18,7 +17,7 @@ pub struct AppConfig { pub site_path: String, pub s3_bucket: String, pub stack_path: PathBuf, - pub(crate) db_conn: Option>>, + pub db_conn: Option>>, } #[derive(Clone)] @@ -99,55 +98,40 @@ impl AppConfig { ) } - /// Get stack path for a specific component pub fn component_path(&self, component: &str) -> PathBuf { self.stack_path.join(component) } - /// Get binary path for a component pub fn bin_path(&self, component: &str) -> PathBuf { self.stack_path.join("bin").join(component) } - /// Get data path for a component pub fn data_path(&self, component: &str) -> PathBuf { self.stack_path.join("data").join(component) } - /// Get config path for a component pub fn config_path(&self, component: &str) -> PathBuf { self.stack_path.join("conf").join(component) } - /// Get log path for a component pub fn log_path(&self, component: &str) -> PathBuf { self.stack_path.join("logs").join(component) } - /// Load configuration from database - /// Falls back to defaults if database is not yet initialized pub fn from_database(conn: &mut PgConnection) -> Self { - info!("Loading configuration from database..."); + info!("Loading configuration from database"); - // Load all configuration from database let config_map = match Self::load_config_from_db(conn) { Ok(map) => { - info!( - "Successfully loaded {} config values from database", - map.len() - ); + info!("Loaded {} config values from database", map.len()); map } Err(e) => { - warn!( - "Failed to load config from database: {}. Using defaults.", - e - ); + warn!("Failed to load config from database: {}. Using defaults", e); HashMap::new() } }; - // Helper to get config value with fallback let get_str = |key: &str, default: &str| -> String { config_map .get(key) @@ -234,10 +218,8 @@ impl AppConfig { } } - /// Legacy method - reads from .env for backward compatibility - /// Will be deprecated once database setup is complete pub fn from_env() -> Self { - warn!("Loading configuration from environment variables (legacy mode)"); + warn!("Loading configuration from environment variables"); let stack_path = std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); @@ -319,11 +301,9 @@ impl AppConfig { } } - /// Load all configuration from database into a HashMap fn load_config_from_db( conn: &mut PgConnection, ) -> Result, diesel::result::Error> { - // Try to query the server_configuration table let results = diesel::sql_query( "SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration", @@ -338,7 +318,6 @@ impl AppConfig { Ok(map) } - /// Update a configuration value in the database pub fn set_config( &self, conn: &mut PgConnection, @@ -354,24 +333,20 @@ impl AppConfig { Ok(()) } - /// Get a configuration value from the database pub fn get_config( &self, conn: &mut PgConnection, key: &str, fallback: Option<&str>, ) -> Result { - // Use empty string when no fallback is supplied let fallback_str = fallback.unwrap_or(""); - // Define a temporary struct that matches the shape of the query result. #[derive(Debug, QueryableByName)] struct ConfigValue { #[diesel(sql_type = Text)] value: String, } - // Execute the query and map the resulting row to the inner string. let result = diesel::sql_query("SELECT get_config($1, $2) as value") .bind::(key) .bind::(fallback_str) @@ -382,7 +357,6 @@ impl AppConfig { } } -/// Configuration manager for handling .gbot/config.csv files pub struct ConfigManager { conn: Arc>, } @@ -392,21 +366,17 @@ impl ConfigManager { Self { conn } } - /// Watch and sync .gbot/config.csv file for a bot pub fn sync_gbot_config( &self, bot_id: &uuid::Uuid, config_path: &str, ) -> Result { - // Import necessary crates for hashing and file handling use sha2::{Digest, Sha256}; use std::fs; - // Read the config.csv file let content = fs::read_to_string(config_path) .map_err(|e| format!("Failed to read config file: {}", e))?; - // Calculate file hash let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); let file_hash = format!("{:x}", hasher.finalize()); @@ -416,7 +386,6 @@ impl ConfigManager { .lock() .map_err(|e| format!("Failed to acquire lock: {}", e))?; - // Check if file has changed #[derive(QueryableByName)] struct SyncHash { #[diesel(sql_type = Text)] @@ -436,16 +405,13 @@ impl ConfigManager { return Ok(0); } - // Parse CSV and update bot configuration let mut updated = 0; for line in content.lines().skip(1) { - // Skip header let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { let key = parts[0].trim(); let value = parts[1].trim(); - // Insert or update bot configuration diesel::sql_query( "INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) VALUES (gen_random_uuid()::text, $1, $2, $3, 'string') @@ -462,7 +428,6 @@ impl ConfigManager { } } - // Update sync record diesel::sql_query( "INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count) VALUES (gen_random_uuid()::text, $1, $2, $3, 1) @@ -476,10 +441,7 @@ impl ConfigManager { .execute(&mut *conn) .map_err(|e| format!("Failed to update sync record: {}", e))?; - info!( - "Synced {} config values for bot {} from {}", - updated, bot_id, config_path - ); + info!("Synced {} config values for bot {} from {}", updated, bot_id, config_path); Ok(updated) } } diff --git a/src/main.rs b/src/main.rs index 826d8ffe2..bacd87d47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,15 +35,19 @@ mod whatsapp; use crate::auth::auth_handler; use crate::automation::AutomationService; -use crate::bot::{start_session, websocket_handler}; use crate::bootstrap::BootstrapManager; +use crate::bot::{start_session, websocket_handler}; use crate::channels::{VoiceAdapter, WebChannelAdapter}; use crate::config::AppConfig; use crate::drive_monitor::DriveMonitor; #[cfg(feature = "email")] -use crate::email::{get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email}; +use crate::email::{ + get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email, +}; use crate::file::{init_drive, upload_file}; -use crate::llm_legacy::llm_local::{chat_completions_local, embeddings_local, ensure_llama_servers_running}; +use crate::llm_legacy::llm_local::{ + chat_completions_local, embeddings_local, ensure_llama_servers_running, +}; use crate::meet::{voice_start, voice_stop}; use crate::package_manager::InstallMode; use crate::session::{create_session, get_session_history, get_sessions}; @@ -55,23 +59,29 @@ use crate::whatsapp::WhatsAppAdapter; #[actix_web::main] async fn main() -> std::io::Result<()> { let args: Vec = std::env::args().collect(); - + if args.len() > 1 { let command = &args[1]; match command.as_str() { "install" | "remove" | "list" | "status" | "--help" | "-h" => { - match package_manager::cli::run() { + match package_manager::cli::run().await { Ok(_) => return Ok(()), Err(e) => { eprintln!("CLI error: {}", e); - return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("CLI command failed: {}", e))); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("CLI command failed: {}", e), + )); } } } _ => { eprintln!("Unknown command: {}", command); eprintln!("Run 'botserver --help' for usage information"); - return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Unknown command: {}", command))); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Unknown command: {}", command), + )); } } } @@ -80,13 +90,13 @@ async fn main() -> std::io::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); info!("Starting BotServer bootstrap process"); - + let install_mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { InstallMode::Local }; - + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { args.get(idx + 1).cloned() } else { @@ -102,7 +112,8 @@ async fn main() -> std::io::Result<()> { Err(e) => { log::error!("Bootstrap failed: {}", e); info!("Attempting to load configuration from database"); - match diesel::Connection::establish(&format!("postgres://localhost:5432/botserver_db")) { + match diesel::Connection::establish(&format!("postgres://localhost:5432/botserver_db")) + { Ok(mut conn) => AppConfig::from_database(&mut conn), Err(_) => { info!("Database not available, using environment variables as fallback"); @@ -113,22 +124,31 @@ async fn main() -> std::io::Result<()> { }; let config = std::sync::Arc::new(cfg.clone()); - + info!("Establishing database connection to {}", cfg.database_url()); let db_pool = match diesel::Connection::establish(&cfg.database_url()) { Ok(conn) => Arc::new(Mutex::new(conn)), Err(e) => { log::error!("Failed to connect to main database: {}", e); - return Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, format!("Database connection failed: {}", e))); + return Err(std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + format!("Database connection failed: {}", e), + )); } }; let db_custom_pool = db_pool.clone(); info!("Initializing LLM server at {}", cfg.ai.endpoint); - ensure_llama_servers_running().await.expect("Failed to initialize LLM local server"); + ensure_llama_servers_running() + .await + .expect("Failed to initialize LLM local server"); - let cache_url = cfg.config_path("cache").join("redis.conf").display().to_string(); + let cache_url = cfg + .config_path("cache") + .join("redis.conf") + .display() + .to_string(); let redis_client = match redis::Client::open(cache_url.as_str()) { Ok(client) => Some(Arc::new(client)), Err(e) => { @@ -138,18 +158,37 @@ async fn main() -> std::io::Result<()> { }; let tool_manager = Arc::new(tools::ToolManager::new()); - let llm_provider = Arc::new(crate::llm::OpenAIClient::new("empty".to_string(), Some(cfg.ai.endpoint.clone()))); - + let llm_provider = Arc::new(crate::llm::OpenAIClient::new( + "empty".to_string(), + Some(cfg.ai.endpoint.clone()), + )); + let web_adapter = Arc::new(WebChannelAdapter::new()); - let voice_adapter = Arc::new(VoiceAdapter::new("https://livekit.example.com".to_string(), "api_key".to_string(), "api_secret".to_string())); - let whatsapp_adapter = Arc::new(WhatsAppAdapter::new("whatsapp_token".to_string(), "phone_number_id".to_string(), "verify_token".to_string())); + let voice_adapter = Arc::new(VoiceAdapter::new( + "https://livekit.example.com".to_string(), + "api_key".to_string(), + "api_secret".to_string(), + )); + let whatsapp_adapter = Arc::new(WhatsAppAdapter::new( + "whatsapp_token".to_string(), + "phone_number_id".to_string(), + "verify_token".to_string(), + )); let tool_api = Arc::new(tools::ToolApi::new()); info!("Initializing MinIO drive at {}", cfg.minio.server); - let drive = init_drive(&config.minio).await.expect("Failed to initialize Drive"); + let drive = init_drive(&config.minio) + .await + .expect("Failed to initialize Drive"); - let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(diesel::Connection::establish(&cfg.database_url()).unwrap(), redis_client.clone()))); - let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new(diesel::Connection::establish(&cfg.database_url()).unwrap(), redis_client.clone()))); + let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( + diesel::Connection::establish(&cfg.database_url()).unwrap(), + redis_client.clone(), + ))); + let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new( + diesel::Connection::establish(&cfg.database_url()).unwrap(), + redis_client.clone(), + ))); let app_state = Arc::new(AppState { s3_client: Some(drive.clone()), @@ -163,7 +202,10 @@ async fn main() -> std::io::Result<()> { auth_service: auth_service.clone(), channels: Arc::new(Mutex::new({ let mut map = HashMap::new(); - map.insert("web".to_string(), web_adapter.clone() as Arc); + map.insert( + "web".to_string(), + web_adapter.clone() as Arc, + ); map })), response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())), @@ -173,12 +215,20 @@ async fn main() -> std::io::Result<()> { tool_api: tool_api.clone(), }); - info!("Starting HTTP server on {}:{}", config.server.host, config.server.port); + info!( + "Starting HTTP server on {}:{}", + config.server.host, config.server.port + ); - let worker_count = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4); + let worker_count = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4); let automation_state = app_state.clone(); - let automation = AutomationService::new(automation_state, "templates/announcements.gbai/announcements.gbdialog"); + let automation = AutomationService::new( + automation_state, + "templates/announcements.gbai/announcements.gbdialog", + ); let _automation_handle = automation.spawn(); let drive_state = app_state.clone(); @@ -187,9 +237,13 @@ async fn main() -> std::io::Result<()> { let _drive_handle = drive_monitor.spawn(); HttpServer::new(move || { - let cors = Cors::default().allow_any_origin().allow_any_method().allow_any_header().max_age(3600); + let cors = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600); let app_state_clone = app_state.clone(); - + let mut app = App::new() .wrap(cors) .wrap(Logger::default()) diff --git a/src/package_manager/mod.rs b/src/package_manager/mod.rs index 902defd83..2f0186fe3 100644 --- a/src/package_manager/mod.rs +++ b/src/package_manager/mod.rs @@ -1,8 +1,10 @@ -use anyhow::{Context, Result}; -use log::{debug, info, trace, warn}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::process::Command; +pub mod component; +pub mod installer; +pub mod os; + +pub use installer::PackageManager; +pub mod cli; +pub mod facade; #[derive(Debug, Clone, PartialEq)] pub enum InstallMode { @@ -16,1436 +18,3 @@ pub enum OsType { MacOS, Windows, } - -#[derive(Debug, Clone)] -pub struct ComponentConfig { - pub name: String, - pub required: bool, - pub ports: Vec, - pub dependencies: Vec, - pub linux_packages: Vec, - pub macos_packages: Vec, - pub windows_packages: Vec, - pub download_url: Option, - pub binary_name: Option, - pub pre_install_cmds_linux: Vec, - pub post_install_cmds_linux: Vec, - pub pre_install_cmds_macos: Vec, - pub post_install_cmds_macos: Vec, - pub pre_install_cmds_windows: Vec, - pub post_install_cmds_windows: Vec, - pub env_vars: HashMap, - pub exec_cmd: String, -} - -pub struct PackageManager { - mode: InstallMode, - os_type: OsType, - base_path: PathBuf, - tenant: String, - components: HashMap, -} - -impl PackageManager { - pub fn new(mode: InstallMode, tenant: Option) -> Result { - let os_type = Self::detect_os(); - let base_path = if mode == InstallMode::Container { - PathBuf::from("/opt/gbo") - } else { - PathBuf::from("./botserver-stack") - }; - - let tenant = tenant.unwrap_or_else(|| "default".to_string()); - - let mut pm = PackageManager { - mode, - os_type, - base_path, - tenant, - components: HashMap::new(), - }; - - pm.register_components(); - info!( - "PackageManager initialized with {} components in {:?} mode for tenant {}", - pm.components.len(), - pm.mode, - pm.tenant - ); - Ok(pm) - } - - fn detect_os() -> OsType { - if cfg!(target_os = "linux") { - OsType::Linux - } else if cfg!(target_os = "macos") { - OsType::MacOS - } else if cfg!(target_os = "windows") { - OsType::Windows - } else { - OsType::Linux - } - } - - fn register_components(&mut self) { - self.register_drive(); - self.register_cache(); - self.register_tables(); - self.register_llm(); - self.register_email(); - self.register_proxy(); - self.register_directory(); - self.register_alm(); - self.register_alm_ci(); - self.register_dns(); - self.register_webmail(); - self.register_meeting(); - self.register_table_editor(); - self.register_doc_editor(); - self.register_desktop(); - self.register_devtools(); - self.register_bot(); - self.register_system(); - self.register_vector_db(); - self.register_host(); - } - - fn register_drive(&mut self) { - self.components.insert("drive".to_string(), ComponentConfig { - name: "drive".to_string(), - required: true, - ports: vec![9000, 9001], - dependencies: vec![], - linux_packages: vec!["wget".to_string()], - macos_packages: vec!["wget".to_string()], - windows_packages: vec![], - download_url: Some("https://dl.min.io/server/minio/release/linux-amd64/minio".to_string()), - binary_name: Some("minio".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "wget https://dl.min.io/client/mc/release/linux-amd64/mc -O {{BIN_PATH}}/mc".to_string(), - "chmod +x {{BIN_PATH}}/mc".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![ - "wget https://dl.min.io/client/mc/release/darwin-amd64/mc -O {{BIN_PATH}}/mc".to_string(), - "chmod +x {{BIN_PATH}}/mc".to_string() - ], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::from([ - ("MINIO_ROOT_USER".to_string(), "minioadmin".to_string()), - ("MINIO_ROOT_PASSWORD".to_string(), "minioadmin".to_string()) - ]), - exec_cmd: "{{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001".to_string(), - }); - } - - fn register_cache(&mut self) { - self.components.insert("cache".to_string(), ComponentConfig { - name: "cache".to_string(), - required: true, - ports: vec![6379], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "curl".to_string(), "gnupg".to_string(), "lsb-release".to_string()], - macos_packages: vec!["redis".to_string()], - windows_packages: vec![], - download_url: None, - binary_name: Some("valkey-server".to_string()), - pre_install_cmds_linux: vec![ - "curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/valkey.gpg".to_string(), - "echo 'deb [signed-by=/usr/share/keyrings/valkey.gpg] https://packages.redis.io/deb $(lsb_release -cs) main' | tee /etc/apt/sources.list.d/valkey.list".to_string(), - "apt-get update && apt-get install -y valkey".to_string() - ], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "valkey-server --port 6379 --dir {{DATA_PATH}}".to_string(), - }); - } - - fn register_tables(&mut self) { - self.components.insert("tables".to_string(), ComponentConfig { - name: "tables".to_string(), - required: true, - ports: vec![5432], - dependencies: vec![], - linux_packages: vec!["wget".to_string()], - macos_packages: vec!["wget".to_string()], - windows_packages: vec![], - download_url: Some("https://github.com/theseus-rs/postgresql-binaries/releases/download/18.0.0/postgresql-18.0.0-x86_64-unknown-linux-gnu.tar.gz".to_string()), - binary_name: Some("postgres".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "tar -xzf postgresql-18.0.0-x86_64-unknown-linux-gnu.tar.gz".to_string(), - "mv pgsql/* . && rm -rf pgsql".to_string(), - "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./initdb -D {{DATA_PATH}}/pgdata -U postgres; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"data_directory = '{{DATA_PATH}}/pgdata'\" > {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"hba_file = '{{CONF_PATH}}/pg_hba.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"ident_file = '{{CONF_PATH}}/pg_ident.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"port = 5432\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"listen_addresses = '*'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"log_directory = '{{LOGS_PATH}}'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"logging_collector = on\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/pg_hba.conf\" ]; then echo \"host all all all md5\" > {{CONF_PATH}}/pg_hba.conf; fi".to_string(), - "if [ ! -f \"{{CONF_PATH}}/pg_ident.conf\" ]; then touch {{CONF_PATH}}/pg_ident.conf; fi".to_string(), - "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start; sleep 5; ./psql -p 5432 -d postgres -c \"CREATE USER default WITH PASSWORD 'defaultpass'\"; ./psql -p 5432 -d postgres -c \"CREATE DATABASE default_db OWNER default\"; ./psql -p 5432 -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE default_db TO default\"; ./pg_ctl -D {{DATA_PATH}}/pgdata stop; fi".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![ - "tar -xzf postgresql-18.0-1-linux-x64-binaries.tar.gz".to_string(), - "mv pgsql/* . && rm -rf pgsql".to_string(), - "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./initdb -D {{DATA_PATH}}/pgdata -U postgres; fi".to_string(), - ], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "./pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start".to_string(), - }); - } - - fn register_llm(&mut self) { - self.components.insert("llm".to_string(), ComponentConfig { - name: "llm".to_string(), - required: true, - ports: vec![8081], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "unzip".to_string()], - macos_packages: vec!["wget".to_string(), "unzip".to_string()], - windows_packages: vec![], - download_url: Some("https://github.com/ggml-org/llama.cpp/releases/download/b6148/llama-b6148-bin-ubuntu-x64.zip".to_string()), - binary_name: Some("llama-server".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "wget https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf -P {{DATA_PATH}}".to_string(), - "wget https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/main/bge-small-en-v1.5-f32.gguf -P {{DATA_PATH}}".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![ - "wget https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf -P {{DATA_PATH}}".to_string(), - "wget https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/main/bge-small-en-v1.5-f32.gguf -P {{DATA_PATH}}".to_string() - ], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/llama-server -m {{DATA_PATH}}/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf --port 8081".to_string(), - }); - } - - fn register_email(&mut self) { - self.components.insert("email".to_string(), ComponentConfig { - name: "email".to_string(), - required: false, - ports: vec![25, 80, 110, 143, 465, 587, 993, 995, 4190], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "libcap2-bin".to_string(), "resolvconf".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("https://github.com/stalwartlabs/stalwart/releases/download/v0.13.1/stalwart-x86_64-unknown-linux-gnu.tar.gz".to_string()), - binary_name: Some("stalwart".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "setcap 'cap_net_bind_service=+ep' {{BIN_PATH}}/stalwart".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/stalwart --config {{CONF_PATH}}/config.toml".to_string(), - }); - } - - fn register_proxy(&mut self) { - self.components.insert("proxy".to_string(), ComponentConfig { - name: "proxy".to_string(), - required: false, - ports: vec![80, 443], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "libcap2-bin".to_string()], - macos_packages: vec!["wget".to_string()], - windows_packages: vec![], - download_url: Some("https://github.com/caddyserver/caddy/releases/download/v2.10.0-beta.3/caddy_2.10.0-beta.3_linux_amd64.tar.gz".to_string()), - binary_name: Some("caddy".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "setcap 'cap_net_bind_service=+ep' {{BIN_PATH}}/caddy".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::from([ - ("XDG_DATA_HOME".to_string(), "{{DATA_PATH}}".to_string()) - ]), - exec_cmd: "{{BIN_PATH}}/caddy run --config {{CONF_PATH}}/Caddyfile".to_string(), - }); - } - - fn register_directory(&mut self) { - self.components.insert("directory".to_string(), ComponentConfig { - name: "directory".to_string(), - required: false, - ports: vec![8080], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "libcap2-bin".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("https://github.com/zitadel/zitadel/releases/download/v2.71.2/zitadel-linux-amd64.tar.gz".to_string()), - binary_name: Some("zitadel".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "setcap 'cap_net_bind_service=+ep' {{BIN_PATH}}/zitadel".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/zitadel start --config {{CONF_PATH}}/zitadel.yaml".to_string(), - }); - } - - fn register_alm(&mut self) { - self.components.insert("alm".to_string(), ComponentConfig { - name: "alm".to_string(), - required: false, - ports: vec![3000], - dependencies: vec![], - linux_packages: vec!["git".to_string(), "git-lfs".to_string(), "wget".to_string()], - macos_packages: vec!["git".to_string(), "git-lfs".to_string()], - windows_packages: vec![], - download_url: Some("https://codeberg.org/forgejo/forgejo/releases/download/v10.0.2/forgejo-10.0.2-linux-amd64".to_string()), - binary_name: Some("forgejo".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::from([ - ("USER".to_string(), "alm".to_string()), - ("HOME".to_string(), "{{DATA_PATH}}".to_string()) - ]), - exec_cmd: "{{BIN_PATH}}/forgejo web --work-path {{DATA_PATH}}".to_string(), - }); - } - - fn register_alm_ci(&mut self) { - self.components.insert("alm-ci".to_string(), ComponentConfig { - name: "alm-ci".to_string(), - required: false, - ports: vec![], - dependencies: vec!["alm".to_string()], - linux_packages: vec!["wget".to_string(), "git".to_string(), "curl".to_string(), "gnupg".to_string(), "ca-certificates".to_string(), "build-essential".to_string()], - macos_packages: vec!["git".to_string(), "node".to_string()], - windows_packages: vec![], - download_url: Some("https://code.forgejo.org/forgejo/runner/releases/download/v6.3.1/forgejo-runner-6.3.1-linux-amd64".to_string()), - binary_name: Some("forgejo-runner".to_string()), - pre_install_cmds_linux: vec![ - "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -".to_string(), - "apt-get update && apt-get install -y nodejs".to_string() - ], - post_install_cmds_linux: vec![ - "npm install -g pnpm@latest".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![ - "npm install -g pnpm@latest".to_string() - ], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/forgejo-runner daemon --config {{CONF_PATH}}/config.yaml".to_string(), - }); - } - - fn register_dns(&mut self) { - self.components.insert("dns".to_string(), ComponentConfig { - name: "dns".to_string(), - required: false, - ports: vec![53], - dependencies: vec![], - linux_packages: vec!["wget".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("https://github.com/coredns/coredns/releases/download/v1.12.4/coredns_1.12.4_linux_amd64.tgz".to_string()), - binary_name: Some("coredns".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![ - "setcap cap_net_bind_service=+ep {{BIN_PATH}}/coredns".to_string() - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/coredns -conf {{CONF_PATH}}/Corefile".to_string(), - }); - } - - fn register_webmail(&mut self) { - self.components.insert("webmail".to_string(), ComponentConfig { - name: "webmail".to_string(), - required: false, - ports: vec![8080], - dependencies: vec!["email".to_string()], - linux_packages: vec!["ca-certificates".to_string(), "apt-transport-https".to_string(), "php8.1".to_string(), "php8.1-fpm".to_string()], - macos_packages: vec!["php".to_string()], - windows_packages: vec![], - download_url: Some("https://github.com/roundcube/roundcubemail/releases/download/1.6.6/roundcubemail-1.6.6-complete.tar.gz".to_string()), - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "php -S 0.0.0.0:8080 -t {{DATA_PATH}}/roundcubemail".to_string(), - }); - } - - fn register_meeting(&mut self) { - self.components.insert("meeting".to_string(), ComponentConfig { - name: "meeting".to_string(), - required: false, - ports: vec![7880, 3478], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "coturn".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("https://github.com/livekit/livekit/releases/download/v1.8.4/livekit_1.8.4_linux_amd64.tar.gz".to_string()), - binary_name: Some("livekit-server".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/livekit-server --config {{CONF_PATH}}/config.yaml".to_string(), - }); - } - - fn register_table_editor(&mut self) { - self.components.insert( - "table-editor".to_string(), - ComponentConfig { - name: "table-editor".to_string(), - required: false, - ports: vec![5757], - dependencies: vec!["tables".to_string()], - linux_packages: vec!["wget".to_string(), "curl".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("http://get.nocodb.com/linux-x64".to_string()), - binary_name: Some("nocodb".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/nocodb".to_string(), - }, - ); - } - - fn register_doc_editor(&mut self) { - self.components.insert( - "doc-editor".to_string(), - ComponentConfig { - name: "doc-editor".to_string(), - required: false, - ports: vec![9980], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "gnupg".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: Some("coolwsd".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "coolwsd --config-file={{CONF_PATH}}/coolwsd.xml".to_string(), - }, - ); - } - - fn register_desktop(&mut self) { - self.components.insert( - "desktop".to_string(), - ComponentConfig { - name: "desktop".to_string(), - required: false, - ports: vec![3389], - dependencies: vec![], - linux_packages: vec!["xvfb".to_string(), "xrdp".to_string(), "xfce4".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "xrdp --nodaemon".to_string(), - }, - ); - } - - fn register_devtools(&mut self) { - self.components.insert( - "devtools".to_string(), - ComponentConfig { - name: "devtools".to_string(), - required: false, - ports: vec![], - dependencies: vec![], - linux_packages: vec!["xclip".to_string(), "git".to_string(), "curl".to_string()], - macos_packages: vec!["git".to_string()], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }, - ); - } - - fn register_bot(&mut self) { - self.components.insert( - "bot".to_string(), - ComponentConfig { - name: "bot".to_string(), - required: false, - ports: vec![3000], - dependencies: vec![], - linux_packages: vec![ - "curl".to_string(), - "gnupg".to_string(), - "ca-certificates".to_string(), - "git".to_string(), - ], - macos_packages: vec!["node".to_string()], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![ - "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -".to_string(), - "apt-get update && apt-get install -y nodejs".to_string(), - ], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::from([("DISPLAY".to_string(), ":99".to_string())]), - exec_cmd: "".to_string(), - }, - ); - } - - fn register_system(&mut self) { - self.components.insert( - "system".to_string(), - ComponentConfig { - name: "system".to_string(), - required: false, - ports: vec![8000], - dependencies: vec![], - linux_packages: vec![ - "wget".to_string(), - "curl".to_string(), - "unzip".to_string(), - "git".to_string(), - ], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }, - ); - } - - fn register_vector_db(&mut self) { - self.components.insert("vector-db".to_string(), ComponentConfig { - name: "vector-db".to_string(), - required: false, - ports: vec![6333], - dependencies: vec![], - linux_packages: vec!["wget".to_string()], - macos_packages: vec!["wget".to_string()], - windows_packages: vec![], - download_url: Some("https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-gnu.tar.gz".to_string()), - binary_name: Some("qdrant".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/qdrant --storage-path {{DATA_PATH}}".to_string(), - }); - } - - fn register_host(&mut self) { - self.components.insert( - "host".to_string(), - ComponentConfig { - name: "host".to_string(), - required: false, - ports: vec![], - dependencies: vec![], - linux_packages: vec!["sshfs".to_string(), "bridge-utils".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![ - "echo 'net.ipv4.ip_forward=1' | tee -a /etc/sysctl.conf".to_string(), - "sysctl -p".to_string(), - ], - post_install_cmds_linux: vec![ - "lxd init --auto".to_string(), - "lxc storage create default dir".to_string(), - "lxc profile device add default root disk path=/ pool=default".to_string(), - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }, - ); - } - - pub fn install(&self, component_name: &str) -> Result<()> { - let component = self - .components - .get(component_name) - .context(format!("Component '{}' not found", component_name))?; - - info!( - "Starting installation of component '{}' in {:?} mode", - component_name, self.mode - ); - - for dep in &component.dependencies { - if !self.is_installed(dep) { - warn!("Installing missing dependency: {}", dep); - self.install(dep)?; - } - } - - match self.mode { - InstallMode::Local => self.install_local(component)?, - InstallMode::Container => self.install_container(component)?, - } - - info!( - "Component '{}' installation completed successfully", - component_name - ); - Ok(()) - } - - fn install_local(&self, component: &ComponentConfig) -> Result<()> { - info!( - "Installing component '{}' locally to {}", - component.name, - self.base_path.display() - ); - - self.create_directories(&component.name)?; - - let (pre_cmds, post_cmds) = match self.os_type { - OsType::Linux => ( - &component.pre_install_cmds_linux, - &component.post_install_cmds_linux, - ), - OsType::MacOS => ( - &component.pre_install_cmds_macos, - &component.post_install_cmds_macos, - ), - OsType::Windows => ( - &component.pre_install_cmds_windows, - &component.post_install_cmds_windows, - ), - }; - - self.run_commands(pre_cmds, "local", &component.name)?; - self.install_system_packages(component)?; - - if let Some(url) = &component.download_url { - self.download_and_install(url, &component.name, component.binary_name.as_deref())?; - } - - self.run_commands(post_cmds, "local", &component.name)?; - - if self.os_type == OsType::Linux && !component.exec_cmd.is_empty() { - self.create_service_file(&component.name, &component.exec_cmd, &component.env_vars)?; - } - - info!("Local installation of '{}' completed", component.name); - Ok(()) - } - - fn install_container(&self, component: &ComponentConfig) -> Result<()> { - let container_name = format!("{}-{}", self.tenant, component.name); - info!("Creating LXC container: {}", container_name); - - let output = Command::new("lxc") - .args(&[ - "launch", - "images:debian/12", - &container_name, - "-c", - "security.privileged=true", - ]) - .output()?; - - if !output.status.success() { - return Err(anyhow::anyhow!( - "LXC container creation failed: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - std::thread::sleep(std::time::Duration::from_secs(15)); - - self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?; - - let (pre_cmds, post_cmds) = match self.os_type { - OsType::Linux => ( - &component.pre_install_cmds_linux, - &component.post_install_cmds_linux, - ), - OsType::MacOS => ( - &component.pre_install_cmds_macos, - &component.post_install_cmds_macos, - ), - OsType::Windows => ( - &component.pre_install_cmds_windows, - &component.post_install_cmds_windows, - ), - }; - - self.run_commands(pre_cmds, &container_name, &component.name)?; - - let packages = match self.os_type { - OsType::Linux => &component.linux_packages, - OsType::MacOS => &component.macos_packages, - OsType::Windows => &component.windows_packages, - }; - - if !packages.is_empty() { - let pkg_list = packages.join(" "); - self.exec_in_container( - &container_name, - &format!("apt-get update && apt-get install -y {}", pkg_list), - )?; - } - - if let Some(url) = &component.download_url { - self.download_in_container( - &container_name, - url, - &component.name, - component.binary_name.as_deref(), - )?; - } - - self.run_commands(post_cmds, &container_name, &component.name)?; - - self.exec_in_container( - &container_name, - "useradd --system --no-create-home --shell /bin/false gbuser", - )?; - self.mount_container_directories(&container_name, &component.name)?; - - if !component.exec_cmd.is_empty() { - self.create_container_service( - &container_name, - &component.name, - &component.exec_cmd, - &component.env_vars, - )?; - } - - self.setup_port_forwarding(&container_name, &component.ports)?; - - info!( - "Container installation of '{}' completed in {}", - component.name, container_name - ); - Ok(()) - } - - pub fn remove(&self, component_name: &str) -> Result<()> { - let component = self - .components - .get(component_name) - .context(format!("Component '{}' not found", component_name))?; - - info!("Removing component: {}", component_name); - - match self.mode { - InstallMode::Local => self.remove_local(component)?, - InstallMode::Container => self.remove_container(component)?, - } - - info!("Component '{}' removed successfully", component_name); - Ok(()) - } - - fn remove_local(&self, component: &ComponentConfig) -> Result<()> { - if component.name == "tables" { - // Stop PostgreSQL if running - let bin_path = self.base_path.join("bin").join(&component.name); - let data_path = self.base_path.join("data").join(&component.name); - - let _ = Command::new(bin_path.join("pg_ctl")) - .args(&["-D", data_path.join("pgdata").to_str().unwrap(), "stop"]) - .output(); - } - - if self.os_type == OsType::Linux { - let _ = Command::new("systemctl") - .args(&["stop", &format!("{}.service", component.name)]) - .output(); - let _ = Command::new("systemctl") - .args(&["disable", &format!("{}.service", component.name)]) - .output(); - let service_path = format!("/etc/systemd/system/{}.service", component.name); - let _ = std::fs::remove_file(service_path); - let _ = Command::new("systemctl").args(&["daemon-reload"]).output(); - } - - let bin_path = self.base_path.join("bin").join(&component.name); - let _ = std::fs::remove_dir_all(bin_path); - - Ok(()) - } - - fn remove_container(&self, component: &ComponentConfig) -> Result<()> { - let container_name = format!("{}-{}", self.tenant, component.name); - - let _ = Command::new("lxc") - .args(&["stop", &container_name]) - .output(); - let output = Command::new("lxc") - .args(&["delete", &container_name]) - .output()?; - - if !output.status.success() { - warn!( - "Container deletion had issues: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - Ok(()) - } - - pub fn list(&self) -> Vec { - self.components.keys().cloned().collect() - } - - pub fn is_installed(&self, component_name: &str) -> bool { - match self.mode { - InstallMode::Local => { - let bin_path = self.base_path.join("bin").join(component_name); - bin_path.exists() - } - InstallMode::Container => { - let container_name = format!("{}-{}", self.tenant, component_name); - let output = Command::new("lxc") - .args(&["list", &container_name, "--format=json"]) - .output() - .unwrap(); - - if !output.status.success() { - return false; - } - - // Parse JSON output to check if container exists and is running - let output_str = String::from_utf8_lossy(&output.stdout); - !output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"") - } - } - } - - fn create_directories(&self, component: &str) -> Result<()> { - let dirs = ["bin", "data", "conf", "logs"]; - for dir in &dirs { - let path = self.base_path.join(dir).join(component); - std::fs::create_dir_all(&path) - .context(format!("Failed to create directory: {:?}", path))?; - trace!("Created directory: {:?}", path); - } - Ok(()) - } - - fn install_system_packages(&self, component: &ComponentConfig) -> Result<()> { - let packages = match self.os_type { - OsType::Linux => &component.linux_packages, - OsType::MacOS => &component.macos_packages, - OsType::Windows => &component.windows_packages, - }; - - if packages.is_empty() { - return Ok(()); - } - - info!( - "Installing {} system packages for component '{}'", - packages.len(), - component.name - ); - - match self.os_type { - OsType::Linux => { - let output = Command::new("apt-get").args(&["update"]).output()?; - - if !output.status.success() { - warn!("apt-get update had issues"); - } - - let output = Command::new("apt-get") - .args(&["install", "-y"]) - .args(packages) - .output()?; - if !output.status.success() { - warn!("Some packages may have failed to install"); - } - } - OsType::MacOS => { - let output = Command::new("brew") - .args(&["install"]) - .args(packages) - .output()?; - if !output.status.success() { - warn!("Homebrew installation had warnings"); - } - } - OsType::Windows => { - warn!("Windows package installation not implemented"); - } - } - - Ok(()) - } - - fn download_and_install( - &self, - url: &str, - component: &str, - binary_name: Option<&str>, - ) -> Result<()> { - let bin_path = self.base_path.join("bin").join(component); - - // Ensure the bin directory exists - std::fs::create_dir_all(&bin_path)?; - - let filename = url.split('/').last().unwrap_or("download.tmp"); - let temp_file = bin_path.join(filename); - - info!("Downloading from: {} to {:?}", url, temp_file); - - // Download to the component's bin directory - let output = Command::new("wget") - .current_dir(&bin_path) - .args(&["-O", temp_file.to_str().unwrap(), url]) - .output()?; - - if !output.status.success() { - return Err(anyhow::anyhow!("Download failed from URL: {}", url)); - } - - if url.ends_with(".tar.gz") || url.ends_with(".tgz") { - trace!("Extracting tar.gz archive to {:?}", bin_path); - let output = Command::new("tar") - .current_dir(&bin_path) - .args(&["-xzf", temp_file.to_str().unwrap()]) - .output()?; - - if !output.status.success() { - return Err(anyhow::anyhow!( - "Extraction failed: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - // Clean up the downloaded archive - std::fs::remove_file(&temp_file)?; - } else if url.ends_with(".zip") { - trace!("Extracting zip archive to {:?}", bin_path); - let output = Command::new("unzip") - .current_dir(&bin_path) - .args(&["-o", temp_file.to_str().unwrap()]) - .output()?; - - if !output.status.success() { - return Err(anyhow::anyhow!( - "Extraction failed: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - std::fs::remove_file(&temp_file)?; - } else if let Some(name) = binary_name { - let final_path = bin_path.join(name); - std::fs::rename(&temp_file, &final_path)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(&final_path)?.permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&final_path, perms)?; - } - } - - Ok(()) - } - - fn create_service_file( - &self, - component: &str, - exec_cmd: &str, - env_vars: &HashMap, - ) -> Result<()> { - let service_path = format!("/etc/systemd/system/{}.service", component); - - let bin_path = self.base_path.join("bin").join(component); - let data_path = self.base_path.join("data").join(component); - let conf_path = self.base_path.join("conf").join(component); - let logs_path = self.base_path.join("logs").join(component); - - // Ensure all directories exist - std::fs::create_dir_all(&bin_path)?; - std::fs::create_dir_all(&data_path)?; - std::fs::create_dir_all(&conf_path)?; - std::fs::create_dir_all(&logs_path)?; - - let rendered_cmd = exec_cmd - .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) - .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) - .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) - .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); - - let mut env_section = String::new(); - for (key, value) in env_vars { - let rendered_value = value - .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) - .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) - .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) - .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); - env_section.push_str(&format!("Environment={}={}\n", key, rendered_value)); - } - - let service_content = format!( - "[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory={}\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", - component, env_section, rendered_cmd, data_path.to_string_lossy() - ); - - std::fs::write(&service_path, service_content)?; - - Command::new("systemctl") - .args(&["daemon-reload"]) - .output()?; - Command::new("systemctl") - .args(&["enable", &format!("{}.service", component)]) - .output()?; - Command::new("systemctl") - .args(&["start", &format!("{}.service", component)]) - .output()?; - - info!("Created and started systemd service: {}.service", component); - Ok(()) - } - - fn run_commands(&self, commands: &[String], target: &str, component: &str) -> Result<()> { - let bin_path = if target == "local" { - self.base_path.join("bin").join(component) - } else { - PathBuf::from("/opt/gbo/bin") - }; - - let data_path = if target == "local" { - self.base_path.join("data").join(component) - } else { - PathBuf::from("/opt/gbo/data") - }; - - let conf_path = if target == "local" { - self.base_path.join("conf").join(component) - } else { - PathBuf::from("/opt/gbo/conf") - }; - - let logs_path = if target == "local" { - self.base_path.join("logs").join(component) - } else { - PathBuf::from("/opt/gbo/logs") - }; - - for cmd in commands { - let rendered_cmd = cmd - .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) - .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) - .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) - .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); - - trace!("Executing command: {}", rendered_cmd); - - if target == "local" { - // Run commands in the component's bin directory - let output = Command::new("bash") - .current_dir(&bin_path) - .args(&["-c", &rendered_cmd]) - .output()?; - if !output.status.success() { - warn!( - "Command had non-zero exit: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - } else { - self.exec_in_container(target, &rendered_cmd)?; - } - } - Ok(()) - } - - fn exec_in_container(&self, container: &str, command: &str) -> Result<()> { - debug!("Executing in container {}: {}", container, command); - let output = Command::new("lxc") - .args(&["exec", container, "--", "bash", "-c", command]) - .output()?; - - if !output.status.success() { - warn!( - "Container command failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - Ok(()) - } - - fn download_in_container( - &self, - container: &str, - url: &str, - _component: &str, - binary_name: Option<&str>, - ) -> Result<()> { - let download_cmd = format!("wget -O /tmp/download.tmp {}", url); - self.exec_in_container(container, &download_cmd)?; - - if url.ends_with(".tar.gz") || url.ends_with(".tgz") { - self.exec_in_container(container, "tar -xzf /tmp/download.tmp -C /opt/gbo/bin")?; - } else if url.ends_with(".zip") { - self.exec_in_container(container, "unzip -o /tmp/download.tmp -d /opt/gbo/bin")?; - } else if let Some(name) = binary_name { - let mv_cmd = format!( - "mv /tmp/download.tmp /opt/gbo/bin/{} && chmod +x /opt/gbo/bin/{}", - name, name - ); - self.exec_in_container(container, &mv_cmd)?; - } - - self.exec_in_container(container, "rm -f /tmp/download.tmp")?; - Ok(()) - } - - fn mount_container_directories(&self, container: &str, component: &str) -> Result<()> { - let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component); - - for dir in &["data", "conf", "logs"] { - let host_path = format!("{}/{}", host_base, dir); - std::fs::create_dir_all(&host_path)?; - - let device_name = format!("{}-{}", component, dir); - let container_path = format!("/opt/gbo/{}", dir); - - let _ = Command::new("lxc") - .args(&["config", "device", "remove", container, &device_name]) - .output(); - - let output = Command::new("lxc") - .args(&[ - "config", - "device", - "add", - container, - &device_name, - "disk", - &format!("source={}", host_path), - &format!("path={}", container_path), - ]) - .output()?; - - if !output.status.success() { - warn!("Failed to mount {} in container {}", dir, container); - } - - trace!( - "Mounted {} to {} in container {}", - host_path, - container_path, - container - ); - } - Ok(()) - } - - fn create_container_service( - &self, - container: &str, - component: &str, - exec_cmd: &str, - env_vars: &HashMap, - ) -> Result<()> { - let rendered_cmd = exec_cmd - .replace("{{BIN_PATH}}", "/opt/gbo/bin") - .replace("{{DATA_PATH}}", "/opt/gbo/data") - .replace("{{CONF_PATH}}", "/opt/gbo/conf") - .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); - - let mut env_section = String::new(); - for (key, value) in env_vars { - let rendered_value = value - .replace("{{DATA_PATH}}", "/opt/gbo/data") - .replace("{{BIN_PATH}}", "/opt/gbo/bin") - .replace("{{CONF_PATH}}", "/opt/gbo/conf") - .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); - env_section.push_str(&format!("Environment={}={}\n", key, rendered_value)); - } - - let service_content = format!( - "[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory=/opt/gbo/data\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", - component, env_section, rendered_cmd - ); - - let service_file = format!("/tmp/{}.service", component); - std::fs::write(&service_file, &service_content)?; - - let output = Command::new("lxc") - .args(&[ - "file", - "push", - &service_file, - &format!("{}/etc/systemd/system/{}.service", container, component), - ]) - .output()?; - - if !output.status.success() { - warn!("Failed to push service file to container"); - } - - self.exec_in_container(container, "systemctl daemon-reload")?; - self.exec_in_container(container, &format!("systemctl enable {}", component))?; - self.exec_in_container(container, &format!("systemctl start {}", component))?; - - std::fs::remove_file(&service_file)?; - - info!( - "Created and started service in container {}: {}", - container, component - ); - Ok(()) - } - - fn setup_port_forwarding(&self, container: &str, ports: &[u16]) -> Result<()> { - for port in ports { - let device_name = format!("port-{}", port); - - let _ = Command::new("lxc") - .args(&["config", "device", "remove", container, &device_name]) - .output(); - - let output = Command::new("lxc") - .args(&[ - "config", - "device", - "add", - container, - &device_name, - "proxy", - &format!("listen=tcp:0.0.0.0:{}", port), - &format!("connect=tcp:127.0.0.1:{}", port), - ]) - .output()?; - - if !output.status.success() { - warn!("Failed to setup port forwarding for port {}", port); - } - - trace!( - "Port forwarding configured: {} -> container {}", - port, - container - ); - } - Ok(()) - } -} - -pub mod cli { - use super::*; - use std::env; - - pub fn run() -> Result<()> { - env_logger::init(); - let args: Vec = env::args().collect(); - - if args.len() < 2 { - print_usage(); - return Ok(()); - } - - let command = &args[1]; - - match command.as_str() { - "install" => { - if args.len() < 3 { - eprintln!( - "Usage: botserver install [--container] [--tenant ]" - ); - return Ok(()); - } - - let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { - InstallMode::Container - } else { - InstallMode::Local - }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { - args.get(idx + 1).cloned() - } else { - None - }; - - let pm = PackageManager::new(mode, tenant)?; - pm.install(component)?; - println!("✓ Component '{}' installed successfully", component); - } - "remove" => { - if args.len() < 3 { - eprintln!( - "Usage: botserver remove [--container] [--tenant ]" - ); - return Ok(()); - } - - let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { - InstallMode::Container - } else { - InstallMode::Local - }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { - args.get(idx + 1).cloned() - } else { - None - }; - - let pm = PackageManager::new(mode, tenant)?; - pm.remove(component)?; - println!("✓ Component '{}' removed successfully", component); - } - "list" => { - let mode = if args.contains(&"--container".to_string()) { - InstallMode::Container - } else { - InstallMode::Local - }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { - args.get(idx + 1).cloned() - } else { - None - }; - - let pm = PackageManager::new(mode, tenant)?; - println!("Available components:"); - for component in pm.list() { - let status = if pm.is_installed(&component) { - "✓ installed" - } else { - " available" - }; - println!(" {} {}", status, component); - } - } - "status" => { - if args.len() < 3 { - eprintln!( - "Usage: botserver status [--container] [--tenant ]" - ); - return Ok(()); - } - - let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { - InstallMode::Container - } else { - InstallMode::Local - }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { - args.get(idx + 1).cloned() - } else { - None - }; - - let pm = PackageManager::new(mode, tenant)?; - if pm.is_installed(component) { - println!("✓ Component '{}' is installed", component); - } else { - println!("✗ Component '{}' is not installed", component); - } - } - "--help" | "-h" => { - print_usage(); - } - _ => { - eprintln!("Unknown command: {}", command); - print_usage(); - } - } - - Ok(()) - } - - fn print_usage() { - println!("BotServer Package Manager\n\nUSAGE:\n botserver [options]\n\nCOMMANDS:\n install Install component\n remove Remove component\n list List all components\n status Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list"); - } -}