2025-12-08 14:08:49 -03:00
|
|
|
use crate::package_manager::cache::{CacheResult, DownloadCache};
|
2025-11-22 22:55:35 -03:00
|
|
|
use crate::package_manager::component::ComponentConfig;
|
|
|
|
|
use crate::package_manager::installer::PackageManager;
|
|
|
|
|
use crate::package_manager::InstallMode;
|
|
|
|
|
use crate::package_manager::OsType;
|
2025-12-07 02:13:28 -03:00
|
|
|
use crate::shared::utils::{self, get_database_url_sync, parse_database_url};
|
2025-11-22 22:55:35 -03:00
|
|
|
use anyhow::{Context, Result};
|
2025-12-08 14:08:49 -03:00
|
|
|
use log::{error, info, trace, warn};
|
2025-11-22 22:55:35 -03:00
|
|
|
use reqwest::Client;
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
impl PackageManager {
|
|
|
|
|
pub async fn install(&self, component_name: &str) -> Result<()> {
|
|
|
|
|
let component = self
|
|
|
|
|
.components
|
|
|
|
|
.get(component_name)
|
|
|
|
|
.context(format!("Component '{}' not found", component_name))?;
|
|
|
|
|
trace!(
|
|
|
|
|
"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);
|
|
|
|
|
Box::pin(self.install(dep)).await?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match self.mode {
|
|
|
|
|
InstallMode::Local => self.install_local(component).await?,
|
|
|
|
|
InstallMode::Container => self.install_container(component)?,
|
|
|
|
|
}
|
|
|
|
|
trace!(
|
|
|
|
|
"Component '{}' installation completed successfully",
|
|
|
|
|
component_name
|
|
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub async fn install_local(&self, component: &ComponentConfig) -> Result<()> {
|
|
|
|
|
trace!(
|
|
|
|
|
"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 {
|
|
|
|
|
let url = url.clone();
|
|
|
|
|
let name = component.name.clone();
|
|
|
|
|
let binary_name = component.binary_name.clone();
|
|
|
|
|
self.download_and_install(&url, &name, binary_name.as_deref())
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
|
|
|
|
if !component.data_download_list.is_empty() {
|
2025-12-08 14:08:49 -03:00
|
|
|
// Initialize cache for data files (models, etc.)
|
|
|
|
|
let cache_base = self.base_path.parent().unwrap_or(&self.base_path);
|
|
|
|
|
let cache = DownloadCache::new(cache_base).ok();
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
for url in &component.data_download_list {
|
2025-12-08 14:08:49 -03:00
|
|
|
let filename = DownloadCache::extract_filename(url);
|
2025-11-22 22:55:35 -03:00
|
|
|
let output_path = self
|
|
|
|
|
.base_path
|
|
|
|
|
.join("data")
|
|
|
|
|
.join(&component.name)
|
2025-12-08 14:08:49 -03:00
|
|
|
.join(&filename);
|
|
|
|
|
|
|
|
|
|
// Check if already exists at destination
|
|
|
|
|
if output_path.exists() {
|
|
|
|
|
info!("Data file already exists: {:?}", output_path);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure data directory exists
|
|
|
|
|
if let Some(parent) = output_path.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
if let Some(ref c) = cache {
|
|
|
|
|
if let Some(cached_path) = c.get_cached_path(&filename) {
|
|
|
|
|
info!("Using cached data file: {:?}", cached_path);
|
|
|
|
|
std::fs::copy(&cached_path, &output_path)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Download to cache if available, otherwise directly to destination
|
|
|
|
|
let download_target = if let Some(ref c) = cache {
|
|
|
|
|
c.get_cache_path(&filename)
|
|
|
|
|
} else {
|
|
|
|
|
output_path.clone()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
info!("Downloading data file: {}", url);
|
|
|
|
|
println!("Downloading {}", url);
|
|
|
|
|
utils::download_file(url, download_target.to_str().unwrap()).await?;
|
|
|
|
|
|
|
|
|
|
// Copy from cache to destination if we downloaded to cache
|
|
|
|
|
if cache.is_some() && download_target != output_path {
|
|
|
|
|
std::fs::copy(&download_target, &output_path)?;
|
|
|
|
|
info!("Copied cached file to: {:?}", output_path);
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.run_commands(post_cmds, "local", &component.name)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn install_container(&self, component: &ComponentConfig) -> Result<()> {
|
|
|
|
|
let container_name = format!("{}-{}", self.tenant, component.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(" ");
|
2025-12-08 14:08:49 -03:00
|
|
|
self.exec_in_container(&container_name, &format!("apt-get install -y {}", pkg_list))?;
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
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)?;
|
|
|
|
|
trace!(
|
|
|
|
|
"Container installation of '{}' completed in {}",
|
|
|
|
|
component.name,
|
|
|
|
|
container_name
|
|
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-12-08 14:08:49 -03:00
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
pub fn remove(&self, component_name: &str) -> Result<()> {
|
|
|
|
|
let component = self
|
|
|
|
|
.components
|
|
|
|
|
.get(component_name)
|
|
|
|
|
.context(format!("Component '{}' not found", component_name))?;
|
|
|
|
|
match self.mode {
|
|
|
|
|
InstallMode::Local => self.remove_local(component)?,
|
|
|
|
|
InstallMode::Container => self.remove_container(component)?,
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn remove_local(&self, component: &ComponentConfig) -> Result<()> {
|
|
|
|
|
let bin_path = self.base_path.join("bin").join(&component.name);
|
|
|
|
|
let _ = std::fs::remove_dir_all(bin_path);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub 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<String> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
!output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub 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))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub 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(());
|
|
|
|
|
}
|
|
|
|
|
trace!(
|
|
|
|
|
"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(())
|
|
|
|
|
}
|
|
|
|
|
pub async fn download_and_install(
|
|
|
|
|
&self,
|
|
|
|
|
url: &str,
|
|
|
|
|
component: &str,
|
|
|
|
|
binary_name: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let bin_path = self.base_path.join("bin").join(component);
|
|
|
|
|
std::fs::create_dir_all(&bin_path)?;
|
2025-12-08 14:08:49 -03:00
|
|
|
|
|
|
|
|
// Initialize cache - use parent of base_path (botserver root) for cache
|
|
|
|
|
let cache_base = self.base_path.parent().unwrap_or(&self.base_path);
|
|
|
|
|
let cache = DownloadCache::new(cache_base).unwrap_or_else(|e| {
|
|
|
|
|
warn!("Failed to initialize download cache: {}", e);
|
|
|
|
|
// Create a fallback cache in base_path
|
|
|
|
|
DownloadCache::new(&self.base_path).expect("Failed to create fallback cache")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
let cache_result = cache.resolve_component_url(component, url);
|
|
|
|
|
|
|
|
|
|
let source_file = match cache_result {
|
|
|
|
|
CacheResult::Cached(cached_path) => {
|
|
|
|
|
info!("Using cached file for {}: {:?}", component, cached_path);
|
|
|
|
|
cached_path
|
|
|
|
|
}
|
|
|
|
|
CacheResult::Download {
|
|
|
|
|
url: download_url,
|
|
|
|
|
cache_path,
|
|
|
|
|
} => {
|
|
|
|
|
info!("Downloading {} from {}", component, download_url);
|
|
|
|
|
println!("Downloading {}", download_url);
|
|
|
|
|
|
|
|
|
|
// Download to cache directory
|
|
|
|
|
self.download_with_reqwest(&download_url, &cache_path, component)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
info!("Cached {} to {:?}", component, cache_path);
|
|
|
|
|
cache_path
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
};
|
2025-12-08 14:08:49 -03:00
|
|
|
|
|
|
|
|
// Now extract/install from the source file (either cached or freshly downloaded)
|
|
|
|
|
self.handle_downloaded_file(&source_file, &bin_path, binary_name)?;
|
2025-11-22 22:55:35 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub async fn download_with_reqwest(
|
|
|
|
|
&self,
|
|
|
|
|
url: &str,
|
2025-12-08 14:08:49 -03:00
|
|
|
target_file: &PathBuf,
|
2025-11-22 22:55:35 -03:00
|
|
|
component: &str,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
const MAX_RETRIES: u32 = 3;
|
|
|
|
|
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2);
|
2025-12-08 14:08:49 -03:00
|
|
|
|
|
|
|
|
// Ensure parent directory exists
|
|
|
|
|
if let Some(parent) = target_file.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
let client = Client::builder()
|
|
|
|
|
.timeout(std::time::Duration::from_secs(30))
|
|
|
|
|
.user_agent("botserver-package-manager/1.0")
|
|
|
|
|
.build()?;
|
|
|
|
|
let mut last_error = None;
|
|
|
|
|
for attempt in 0..=MAX_RETRIES {
|
|
|
|
|
if attempt > 0 {
|
|
|
|
|
trace!(
|
|
|
|
|
"Retry attempt {}/{} for {}",
|
|
|
|
|
attempt,
|
|
|
|
|
MAX_RETRIES,
|
|
|
|
|
component
|
|
|
|
|
);
|
|
|
|
|
std::thread::sleep(RETRY_DELAY * attempt);
|
|
|
|
|
}
|
2025-12-08 14:08:49 -03:00
|
|
|
match self
|
|
|
|
|
.attempt_reqwest_download(&client, url, target_file)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-11-22 22:55:35 -03:00
|
|
|
Ok(_size) => {
|
|
|
|
|
if attempt > 0 {
|
|
|
|
|
trace!("Download succeeded on retry attempt {}", attempt);
|
|
|
|
|
}
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!("Download attempt {} failed: {}", attempt + 1, e);
|
|
|
|
|
last_error = Some(e);
|
2025-12-08 14:08:49 -03:00
|
|
|
let _ = std::fs::remove_file(target_file);
|
2025-11-22 22:55:35 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(anyhow::anyhow!(
|
|
|
|
|
"Failed to download {} after {} attempts. Last error: {}",
|
|
|
|
|
component,
|
|
|
|
|
MAX_RETRIES + 1,
|
|
|
|
|
last_error.unwrap()
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
pub async fn attempt_reqwest_download(
|
|
|
|
|
&self,
|
|
|
|
|
_client: &Client,
|
|
|
|
|
url: &str,
|
|
|
|
|
temp_file: &PathBuf,
|
|
|
|
|
) -> Result<u64> {
|
|
|
|
|
let output_path = temp_file.to_str().context("Invalid temp file path")?;
|
|
|
|
|
utils::download_file(url, output_path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Failed to download file using shared utility: {}", e))?;
|
|
|
|
|
let metadata = std::fs::metadata(temp_file).context("Failed to get file metadata")?;
|
|
|
|
|
let size = metadata.len();
|
|
|
|
|
Ok(size)
|
|
|
|
|
}
|
|
|
|
|
pub fn handle_downloaded_file(
|
|
|
|
|
&self,
|
|
|
|
|
temp_file: &PathBuf,
|
|
|
|
|
bin_path: &PathBuf,
|
|
|
|
|
binary_name: Option<&str>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let metadata = std::fs::metadata(temp_file)?;
|
|
|
|
|
if metadata.len() == 0 {
|
|
|
|
|
return Err(anyhow::anyhow!("Downloaded file is empty"));
|
|
|
|
|
}
|
|
|
|
|
let file_extension = temp_file
|
|
|
|
|
.extension()
|
|
|
|
|
.and_then(|ext| ext.to_str())
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
match file_extension {
|
|
|
|
|
"gz" | "tgz" => {
|
|
|
|
|
self.extract_tar_gz(temp_file, bin_path)?;
|
|
|
|
|
}
|
|
|
|
|
"zip" => {
|
|
|
|
|
self.extract_zip(temp_file, bin_path)?;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
if let Some(name) = binary_name {
|
|
|
|
|
self.install_binary(temp_file, bin_path, name)?;
|
|
|
|
|
} else {
|
|
|
|
|
let final_path = bin_path.join(temp_file.file_name().unwrap());
|
2025-12-08 14:19:55 -03:00
|
|
|
// Copy instead of rename if source is in cache directory
|
|
|
|
|
// This preserves cached files for offline installation
|
|
|
|
|
if temp_file.to_string_lossy().contains("botserver-installers") {
|
|
|
|
|
std::fs::copy(temp_file, &final_path)?;
|
|
|
|
|
} else {
|
|
|
|
|
std::fs::rename(temp_file, &final_path)?;
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
self.make_executable(&final_path)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn extract_tar_gz(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
|
|
|
|
let output = Command::new("tar")
|
|
|
|
|
.current_dir(bin_path)
|
|
|
|
|
.args(&["-xzf", temp_file.to_str().unwrap(), "--strip-components=1"])
|
|
|
|
|
.output()?;
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"tar extraction failed: {}",
|
|
|
|
|
String::from_utf8_lossy(&output.stderr)
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-12-08 14:19:55 -03:00
|
|
|
// Only delete if NOT in the cache directory (botserver-installers)
|
|
|
|
|
// Cached files should be preserved for offline installation
|
|
|
|
|
if !temp_file.to_string_lossy().contains("botserver-installers") {
|
|
|
|
|
std::fs::remove_file(temp_file)?;
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn extract_zip(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
|
|
|
|
let output = Command::new("unzip")
|
|
|
|
|
.current_dir(bin_path)
|
|
|
|
|
.args(&["-o", "-q", temp_file.to_str().unwrap()])
|
|
|
|
|
.output()?;
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"unzip extraction failed: {}",
|
|
|
|
|
String::from_utf8_lossy(&output.stderr)
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-12-08 14:19:55 -03:00
|
|
|
// Only delete if NOT in the cache directory (botserver-installers)
|
|
|
|
|
// Cached files should be preserved for offline installation
|
|
|
|
|
if !temp_file.to_string_lossy().contains("botserver-installers") {
|
|
|
|
|
std::fs::remove_file(temp_file)?;
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn install_binary(
|
|
|
|
|
&self,
|
|
|
|
|
temp_file: &PathBuf,
|
|
|
|
|
bin_path: &PathBuf,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let final_path = bin_path.join(name);
|
2025-12-08 14:19:55 -03:00
|
|
|
// Copy instead of rename if source is in cache directory
|
|
|
|
|
// This preserves cached files for offline installation
|
|
|
|
|
if temp_file.to_string_lossy().contains("botserver-installers") {
|
|
|
|
|
std::fs::copy(temp_file, &final_path)?;
|
|
|
|
|
} else {
|
|
|
|
|
std::fs::rename(temp_file, &final_path)?;
|
|
|
|
|
}
|
2025-11-22 22:55:35 -03:00
|
|
|
self.make_executable(&final_path)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn make_executable(&self, path: &PathBuf) -> Result<()> {
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
let mut perms = std::fs::metadata(path)?.permissions();
|
|
|
|
|
perms.set_mode(0o755);
|
|
|
|
|
std::fs::set_permissions(path, perms)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub 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")
|
|
|
|
|
};
|
2025-12-08 00:19:29 -03:00
|
|
|
// CONF_PATH should be the base conf directory, not component-specific
|
|
|
|
|
// Commands that need component subdirs include them explicitly (e.g., {{CONF_PATH}}/directory/zitadel.yaml)
|
2025-11-22 22:55:35 -03:00
|
|
|
let conf_path = if target == "local" {
|
2025-12-08 00:19:29 -03:00
|
|
|
self.base_path.join("conf")
|
2025-11-22 22:55:35 -03:00
|
|
|
} 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")
|
|
|
|
|
};
|
2025-12-08 14:08:49 -03:00
|
|
|
|
2025-12-08 00:19:29 -03:00
|
|
|
// Get DB password from Vault for commands that need it (e.g., PostgreSQL initdb)
|
|
|
|
|
let db_password = match get_database_url_sync() {
|
|
|
|
|
Ok(url) => {
|
|
|
|
|
let (_, password, _, _, _) = parse_database_url(&url);
|
|
|
|
|
password
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
// Vault not available yet - this is OK during early bootstrap
|
|
|
|
|
// Commands that don't need DB_PASSWORD will still work
|
|
|
|
|
trace!("Vault not available for DB_PASSWORD, using empty string");
|
|
|
|
|
String::new()
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-12-08 14:08:49 -03:00
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
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())
|
2025-12-08 00:19:29 -03:00
|
|
|
.replace("{{LOGS_PATH}}", &logs_path.to_string_lossy())
|
|
|
|
|
.replace("{{DB_PASSWORD}}", &db_password);
|
2025-11-22 22:55:35 -03:00
|
|
|
if target == "local" {
|
|
|
|
|
trace!("Executing command: {}", rendered_cmd);
|
2025-12-09 01:09:04 -03:00
|
|
|
let output = Command::new("bash")
|
2025-11-22 22:55:35 -03:00
|
|
|
.current_dir(&bin_path)
|
|
|
|
|
.args(&["-c", &rendered_cmd])
|
2025-12-09 01:09:04 -03:00
|
|
|
.stdout(std::process::Stdio::null())
|
|
|
|
|
.stderr(std::process::Stdio::piped())
|
|
|
|
|
.output()
|
2025-11-22 22:55:35 -03:00
|
|
|
.with_context(|| {
|
2025-12-09 01:09:04 -03:00
|
|
|
format!("Failed to execute command for component '{}'", component)
|
2025-11-22 22:55:35 -03:00
|
|
|
})?;
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
error!(
|
|
|
|
|
"Command had non-zero exit: {}",
|
|
|
|
|
String::from_utf8_lossy(&output.stderr)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.exec_in_container(target, &rendered_cmd)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub fn exec_in_container(&self, container: &str, command: &str) -> Result<()> {
|
|
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
pub 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(())
|
|
|
|
|
}
|
|
|
|
|
pub 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(())
|
|
|
|
|
}
|
|
|
|
|
pub fn create_container_service(
|
|
|
|
|
&self,
|
|
|
|
|
container: &str,
|
|
|
|
|
component: &str,
|
|
|
|
|
exec_cmd: &str,
|
|
|
|
|
env_vars: &HashMap<String, String>,
|
|
|
|
|
) -> Result<()> {
|
2025-12-07 02:13:28 -03:00
|
|
|
let database_url = get_database_url_sync()
|
|
|
|
|
.context("Failed to get DATABASE_URL from Vault. Ensure Vault is configured.")?;
|
2025-11-22 22:55:35 -03:00
|
|
|
let (_db_username, db_password, _db_server, _db_port, _db_name) =
|
|
|
|
|
parse_database_url(&database_url);
|
|
|
|
|
|
|
|
|
|
let rendered_cmd = exec_cmd
|
|
|
|
|
.replace("{{DB_PASSWORD}}", &db_password)
|
|
|
|
|
.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)?;
|
|
|
|
|
trace!(
|
|
|
|
|
"Created and started service in container {}: {}",
|
|
|
|
|
container,
|
|
|
|
|
component
|
|
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
pub 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(())
|
|
|
|
|
}
|
|
|
|
|
}
|