Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Server Manager - Next-Gen Media Server Orchestrator 🚀

![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.7-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue)
![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.8-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue)

**Server Manager** is a powerful and intelligent tool written in Rust to deploy, manage, and optimize a complete personal media and cloud server stack. It detects your hardware and automatically configures 28 Docker services for optimal performance.

Expand All @@ -23,6 +23,7 @@ Welcome to the Server Manager documentation. Whether you are a beginner or an ex
## ✨ Key Features
* **28 Integrated Services**: Plex, ArrStack, Nextcloud, Mailserver, etc.
* **Smart Hardware Detection**: Adapts configuration (RAM, Transcoding, Swap) to your machine (Low/Standard/High Profile).
* **Fast Configuration**: Uses internal orchestration for instant service toggling without subprocess overhead.
* **Secure by Default**: UFW firewall configured, passwords generated, isolated networks.
* **GPU Support**: Automatic detection and configuration for Nvidia & Intel QuickSync.

Expand Down Expand Up @@ -186,6 +187,7 @@ Bienvenue sur la documentation de Server Manager. Que vous soyez débutant ou ex
## ✨ Fonctionnalités Clés
* **28 Services Intégrés** : Plex, ArrStack, Nextcloud, Mailserver, etc.
* **Détection Matérielle Intelligente** : Adapte la configuration (RAM, Transcodage, Swap) selon votre machine (Low/Standard/High Profile).
* **Configuration Rapide** : Utilise une orchestration interne pour une activation instantanée des services.
* **Sécurité par Défaut** : Pare-feu UFW configuré, mots de passe générés, réseaux isolés.
* **Support GPU** : Détection et configuration automatique Nvidia & Intel QuickSync.

Expand Down
2 changes: 1 addition & 1 deletion server_manager/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "server_manager"
version = "1.0.7"
version = "1.0.8"
edition = "2021"

[dependencies]
Expand Down
39 changes: 27 additions & 12 deletions server_manager/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use log::info;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::SystemTime;
use tokio::sync::RwLock;
Expand All @@ -23,11 +23,21 @@ pub struct Config {
}

impl Config {
fn get_config_path() -> PathBuf {
let global_path = Path::new("/opt/server_manager/config.yaml");
let local_path = Path::new("config.yaml");

if global_path.exists() {
global_path.to_path_buf()
} else {
local_path.to_path_buf()
}
}

pub fn load() -> Result<Self> {
let path = Path::new("config.yaml");
let path = Self::get_config_path();
if path.exists() {
let content = fs::read_to_string(path).context("Failed to read config.yaml")?;
// If empty file, return default
let content = fs::read_to_string(&path).context("Failed to read config.yaml")?;
if content.trim().is_empty() {
return Ok(Config::default());
}
Expand All @@ -38,6 +48,7 @@ impl Config {
}

pub async fn load_async() -> Result<Self> {
let path = Self::get_config_path();
let cache = CONFIG_CACHE.get_or_init(|| {
RwLock::new(CachedConfig {
config: Config::default(),
Expand All @@ -49,8 +60,7 @@ impl Config {
{
let guard = cache.read().await;
if let Some(cached_mtime) = guard.last_mtime {
// Check if file still matches
if let Ok(metadata) = tokio::fs::metadata("config.yaml").await {
if let Ok(metadata) = tokio::fs::metadata(&path).await {
if let Ok(modified) = metadata.modified() {
if modified == cached_mtime {
return Ok(guard.config.clone());
Expand All @@ -63,8 +73,7 @@ impl Config {
// Slow path: Update cache
let mut guard = cache.write().await;

// Check metadata again (double-checked locking pattern)
let metadata_res = tokio::fs::metadata("config.yaml").await;
let metadata_res = tokio::fs::metadata(&path).await;

match metadata_res {
Ok(metadata) => {
Expand All @@ -76,8 +85,7 @@ impl Config {
}
}

// Load file
match tokio::fs::read_to_string("config.yaml").await {
match tokio::fs::read_to_string(&path).await {
Ok(content) => {
let config = if content.trim().is_empty() {
Config::default()
Expand All @@ -94,7 +102,6 @@ impl Config {
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// File not found -> Default
guard.config = Config::default();
guard.last_mtime = None;
Ok(guard.config.clone())
Expand All @@ -104,8 +111,16 @@ impl Config {
}

pub fn save(&self) -> Result<()> {
let path = Self::get_config_path();
// If config doesn't exist, try to default to /opt/server_manager if directory exists
let target_path = if !path.exists() && Path::new("/opt/server_manager").exists() {
Path::new("/opt/server_manager/config.yaml")
} else {
path.as_path()
};

let content = serde_yaml_ng::to_string(self)?;
fs::write("config.yaml", content).context("Failed to write config.yaml")?;
fs::write(target_path, content).context("Failed to write config.yaml")?;
Ok(())
}

Expand Down
6 changes: 6 additions & 0 deletions server_manager/src/core/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl HardwareInfo {
sys.refresh_memory();
sys.refresh_cpu();
sys.refresh_disks_list();
sys.refresh_disks();

let total_memory = sys.total_memory(); // Bytes
let ram_gb = total_memory / 1024 / 1024 / 1024;
Expand Down Expand Up @@ -93,6 +94,11 @@ impl HardwareInfo {
}
}

// If running as root (and not via sudo), default to root
if nix::unistd::Uid::effective().is_root() {
return ("0".to_string(), "0".to_string());
}

warn!("SUDO_USER not found or lookup failed. Defaulting to UID/GID 1000.");
("1000".to_string(), "1000".to_string())
}
Expand Down
1 change: 1 addition & 0 deletions server_manager/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod hardware;
pub mod secrets;
pub mod system;
pub mod users;
pub mod orchestrator;
116 changes: 116 additions & 0 deletions server_manager/src/core/orchestrator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use crate::core::{config, hardware, secrets};
use crate::{build_compose_structure, services};
use anyhow::{Context, Result};
use log::{error, info};
use std::process::Stdio;
use tokio::process::Command;

pub async fn apply(
hw: &hardware::HardwareInfo,
secrets: &secrets::Secrets,
config: &config::Config,
) -> Result<()> {
// 1. Configure & Initialize (Blocking IO)
let hw_clone = hw.clone();
let secrets_clone = secrets.clone();
let config_clone = config.clone();

tokio::task::spawn_blocking(move || {
configure_services(&hw_clone, &secrets_clone, &config_clone)?;
initialize_services(&hw_clone, &secrets_clone, &config_clone)?;
generate_compose_file(&hw_clone, &secrets_clone, &config_clone)?;
Ok::<(), anyhow::Error>(())
})
.await
.context("Failed to execute blocking configuration tasks")??;

// 2. Docker Compose Up (Async)
info!("Applying changes via Docker Compose...");
let status = Command::new("docker")
.args(["compose", "up", "-d", "--remove-orphans"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await
.context("Failed to run docker compose up")?;

if status.success() {
info!("Stack deployed successfully!");
} else {
error!("Docker Compose failed.");
anyhow::bail!("Docker Compose failed");
}

Ok(())
}

pub async fn generate_only(
hw: &hardware::HardwareInfo,
secrets: &secrets::Secrets,
config: &config::Config,
) -> Result<()> {
let hw_clone = hw.clone();
let secrets_clone = secrets.clone();
let config_clone = config.clone();

tokio::task::spawn_blocking(move || {
configure_services(&hw_clone, &secrets_clone, &config_clone)?;
generate_compose_file(&hw_clone, &secrets_clone, &config_clone)
})
.await
.context("Failed to execute blocking generation tasks")??;

Ok(())
}

fn configure_services(
hw: &hardware::HardwareInfo,
secrets: &secrets::Secrets,
config: &config::Config,
) -> Result<()> {
info!("Configuring services (generating config files)...");
let services = services::get_all_services();
for service in services {
if !config.is_enabled(service.name()) {
continue;
}
service
.configure(hw, secrets)
.with_context(|| format!("Failed to configure service: {}", service.name()))?;
}
Ok(())
}

fn initialize_services(
hw: &hardware::HardwareInfo,
secrets: &secrets::Secrets,
config: &config::Config,
) -> Result<()> {
info!("Initializing services (system setup)...");
let services = services::get_all_services();
for service in services {
if !config.is_enabled(service.name()) {
continue;
}
service
.initialize(hw, secrets)
.with_context(|| format!("Failed to initialize service: {}", service.name()))?;
}
Ok(())
}

fn generate_compose_file(
hw: &hardware::HardwareInfo,
secrets: &secrets::Secrets,
config: &config::Config,
) -> Result<()> {
info!("Generating docker-compose.yml based on hardware profile...");
let top_level = build_compose_structure(hw, secrets, config)?;
let yaml_output = serde_yaml_ng::to_string(&top_level)?;

std::fs::write("docker-compose.yml", yaml_output)
.context("Failed to write docker-compose.yml")?;
info!("docker-compose.yml generated.");

Ok(())
}
46 changes: 20 additions & 26 deletions server_manager/src/core/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use nix::unistd::Uid;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum Role {
Expand All @@ -29,27 +29,26 @@ pub struct UserManager {
}

impl UserManager {
fn get_users_path() -> PathBuf {
let global_path = Path::new("/opt/server_manager/users.yaml");
let local_path = Path::new("users.yaml");

if global_path.exists() {
global_path.to_path_buf()
} else {
local_path.to_path_buf()
}
}

pub async fn load_async() -> Result<Self> {
tokio::task::spawn_blocking(Self::load).await?
}

pub fn load() -> Result<Self> {
// Try CWD or /opt/server_manager
let path = Path::new("users.yaml");
let fallback_path = Path::new("/opt/server_manager/users.yaml");

// Priority: /opt/server_manager/users.yaml > ./users.yaml
// This aligns with save() behavior which prefers /opt if available.
let load_path = if fallback_path.exists() {
Some(fallback_path)
} else if path.exists() {
Some(path)
} else {
None
};
let path = Self::get_users_path();

let mut manager = if let Some(p) = load_path {
let content = fs::read_to_string(p).context("Failed to read users.yaml")?;
let mut manager = if path.exists() {
let content = fs::read_to_string(&path).context("Failed to read users.yaml")?;
if content.trim().is_empty() {
UserManager::default()
} else {
Expand All @@ -62,12 +61,6 @@ impl UserManager {
// Ensure default admin exists if no users
if manager.users.is_empty() {
info!("No users found. Creating default 'admin' user.");
// We use a generated secret for the initial password if secrets exist,
// otherwise generate one.
// Better: use 'admin' / 'admin' but WARN, or generate random.
// Let's generate a random one and print it, safer.
// Re-using secrets generation logic if possible, or just simple random.
// For simplicity in this context, let's look for a stored password or default to 'admin' and log a warning.

let pass = "admin";
let hash = hash(pass, DEFAULT_COST)?;
Expand All @@ -88,15 +81,16 @@ impl UserManager {
}

pub fn save(&self) -> Result<()> {
// Prefer saving to /opt/server_manager if it exists/is writable, else CWD
let target = if Path::new("/opt/server_manager").exists() {
let path = Self::get_users_path();
// If file doesn't exist, try to default to /opt/server_manager if directory exists
let target_path = if !path.exists() && Path::new("/opt/server_manager").exists() {
Path::new("/opt/server_manager/users.yaml")
} else {
Path::new("users.yaml")
path.as_path()
};

let content = serde_yaml_ng::to_string(self)?;
fs::write(target, content).context("Failed to write users.yaml")?;
fs::write(target_path, content).context("Failed to write users.yaml")?;
Ok(())
}

Expand Down
Loading