diff --git a/server_manager/src/core/system.rs b/server_manager/src/core/system.rs index c81ad38..7fedf20 100644 --- a/server_manager/src/core/system.rs +++ b/server_manager/src/core/system.rs @@ -4,6 +4,7 @@ use std::path::Path; use std::fs; use anyhow::{Result, Context, bail}; use log::{info, warn}; +use sysinfo::{System, SystemExt}; pub fn check_root() -> Result<()> { if !Uid::effective().is_root() { @@ -54,12 +55,23 @@ pub fn install_dependencies() -> Result<()> { pub fn apply_optimizations() -> Result<()> { info!("Applying system optimizations for media server performance..."); - let config = r#"# Server Manager Media Server Optimizations + let mut sys = System::new_all(); + sys.refresh_memory(); + let ram_gb = sys.total_memory() / 1024 / 1024 / 1024; + + // Aggressive swappiness reduction for high RAM + let swappiness = if ram_gb > 16 { 1 } else { 10 }; + + let config = format!(r#"# Server Manager Media Server Optimizations fs.inotify.max_user_watches=524288 -vm.swappiness=10 +vm.swappiness={} +vm.dirty_ratio=10 +vm.dirty_background_ratio=5 net.core.default_qdisc=fq net.ipv4.tcp_congestion_control=bbr -"#; +net.core.somaxconn=4096 +net.ipv4.tcp_fastopen=3 +"#, swappiness); let path = Path::new("/etc/sysctl.d/99-server-manager-optimization.conf"); fs::write(path, config).context("Failed to write sysctl config")?; diff --git a/server_manager/src/lib.rs b/server_manager/src/lib.rs index fbb8e04..d4a5e97 100644 --- a/server_manager/src/lib.rs +++ b/server_manager/src/lib.rs @@ -152,6 +152,54 @@ pub fn build_compose_structure(hw: &hardware::HardwareInfo, secrets: &secrets::S config.insert("sysctls".into(), serde_yaml::Value::Sequence(sys_seq)); } + // Resources (Deploy) + if let Some(res) = service.resources(hw) { + let mut deploy = serde_yaml::Mapping::new(); + let mut resources = serde_yaml::Mapping::new(); + let mut limits = serde_yaml::Mapping::new(); + let mut reservations = serde_yaml::Mapping::new(); + + if let Some(mem) = res.memory_limit { + limits.insert("memory".into(), mem.into()); + } + if let Some(cpu) = res.cpu_limit { + limits.insert("cpus".into(), cpu.into()); + } + + if let Some(mem) = res.memory_reservation { + reservations.insert("memory".into(), mem.into()); + } + if let Some(cpu) = res.cpu_reservation { + reservations.insert("cpus".into(), cpu.into()); + } + + if !limits.is_empty() { + resources.insert("limits".into(), serde_yaml::Value::Mapping(limits)); + } + if !reservations.is_empty() { + resources.insert("reservations".into(), serde_yaml::Value::Mapping(reservations)); + } + + if !resources.is_empty() { + deploy.insert("resources".into(), serde_yaml::Value::Mapping(resources)); + config.insert("deploy".into(), serde_yaml::Value::Mapping(deploy)); + } + } + + // Logging + let logging = service.logging(); + let mut logging_map = serde_yaml::Mapping::new(); + logging_map.insert("driver".into(), logging.driver.into()); + + let mut opts_map = serde_yaml::Mapping::new(); + for (k, v) in logging.options { + opts_map.insert(k.into(), v.into()); + } + if !opts_map.is_empty() { + logging_map.insert("options".into(), serde_yaml::Value::Mapping(opts_map)); + } + config.insert("logging".into(), serde_yaml::Value::Mapping(logging_map)); + compose_services.insert(service.name().to_string(), serde_yaml::Value::Mapping(config)); } diff --git a/server_manager/src/services/arr.rs b/server_manager/src/services/arr.rs index 030ba39..cb4b14d 100644 --- a/server_manager/src/services/arr.rs +++ b/server_manager/src/services/arr.rs @@ -1,4 +1,4 @@ -use super::Service; +use super::{Service, ResourceConfig}; use crate::core::hardware::{HardwareInfo, HardwareProfile}; use crate::core::secrets::Secrets; use std::collections::HashMap; @@ -30,6 +30,19 @@ macro_rules! define_arr_service { fn healthcheck(&self) -> Option { Some(format!("curl -f http://localhost:{}/ping || exit 1", $port)) } + + fn resources(&self, hw: &HardwareInfo) -> Option { + let memory_limit = match hw.profile { + HardwareProfile::High => "2G", + _ => "1G", + }; + Some(ResourceConfig { + memory_limit: Some(memory_limit.to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } }; } diff --git a/server_manager/src/services/infra.rs b/server_manager/src/services/infra.rs index c90a49d..c3acfd4 100644 --- a/server_manager/src/services/infra.rs +++ b/server_manager/src/services/infra.rs @@ -1,5 +1,5 @@ -use super::Service; -use crate::core::hardware::{HardwareInfo}; +use super::{Service, ResourceConfig}; +use crate::core::hardware::{HardwareInfo, HardwareProfile}; use crate::core::secrets::Secrets; use std::collections::HashMap; use anyhow::{Result, Context}; @@ -62,6 +62,21 @@ impl Service for MariaDBService { fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/mariadb:/config".to_string()] } + fn networks(&self) -> Vec { vec!["server_manager_net".to_string()] } + + fn resources(&self, hw: &HardwareInfo) -> Option { + let memory_limit = match hw.profile { + HardwareProfile::High => "4G", + HardwareProfile::Standard => "2G", + HardwareProfile::Low => "512M", + }; + Some(ResourceConfig { + memory_limit: Some(memory_limit.to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } pub struct RedisService; @@ -69,6 +84,22 @@ impl Service for RedisService { fn name(&self) -> &'static str { "redis" } fn image(&self) -> &'static str { "redis:alpine" } fn ports(&self) -> Vec { vec!["6379:6379".to_string()] } + fn volumes(&self, _hw: &HardwareInfo) -> Vec { + vec!["./config/redis:/data".to_string()] + } + fn resources(&self, hw: &HardwareInfo) -> Option { + let memory_limit = match hw.profile { + HardwareProfile::High => "512M", + HardwareProfile::Standard => "256M", + HardwareProfile::Low => "128M", + }; + Some(ResourceConfig { + memory_limit: Some(memory_limit.to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } pub struct NginxProxyService; diff --git a/server_manager/src/services/media.rs b/server_manager/src/services/media.rs index 16e7342..4767b35 100644 --- a/server_manager/src/services/media.rs +++ b/server_manager/src/services/media.rs @@ -1,4 +1,4 @@ -use super::Service; +use super::{Service, ResourceConfig}; use crate::core::hardware::{HardwareInfo, HardwareProfile}; use crate::core::secrets::Secrets; use std::collections::HashMap; @@ -56,6 +56,20 @@ impl Service for PlexService { fn healthcheck(&self) -> Option { Some("curl -f http://localhost:32400/identity || exit 1".to_string()) } + + fn resources(&self, hw: &HardwareInfo) -> Option { + let memory_limit = match hw.profile { + HardwareProfile::High => "8G", + HardwareProfile::Standard => "4G", + HardwareProfile::Low => "2G", + }; + Some(ResourceConfig { + memory_limit: Some(memory_limit.to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } pub struct TautulliService; @@ -67,6 +81,14 @@ impl Service for TautulliService { vec!["./config/tautulli:/config".to_string()] } fn depends_on(&self) -> Vec { vec!["plex".to_string()] } + fn resources(&self, _hw: &HardwareInfo) -> Option { + Some(ResourceConfig { + memory_limit: Some("512M".to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } pub struct OverseerrService; @@ -77,4 +99,12 @@ impl Service for OverseerrService { fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/overseerr:/config".to_string()] } + fn resources(&self, _hw: &HardwareInfo) -> Option { + Some(ResourceConfig { + memory_limit: Some("1G".to_string()), + memory_reservation: None, + cpu_limit: None, + cpu_reservation: None, + }) + } } diff --git a/server_manager/src/services/mod.rs b/server_manager/src/services/mod.rs index 8471c4f..3a6bc67 100644 --- a/server_manager/src/services/mod.rs +++ b/server_manager/src/services/mod.rs @@ -9,6 +9,32 @@ use crate::core::secrets::Secrets; use std::collections::HashMap; use anyhow::Result; +#[derive(Debug, Clone)] +pub struct ResourceConfig { + pub memory_limit: Option, + pub memory_reservation: Option, + pub cpu_limit: Option, + pub cpu_reservation: Option, +} + +#[derive(Debug, Clone)] +pub struct LoggingConfig { + pub driver: String, + pub options: HashMap, +} + +impl Default for LoggingConfig { + fn default() -> Self { + let mut options = HashMap::new(); + options.insert("max-size".to_string(), "10m".to_string()); + options.insert("max-file".to_string(), "3".to_string()); + Self { + driver: "json-file".to_string(), + options, + } + } +} + pub trait Service: Send + Sync { fn name(&self) -> &'static str; fn image(&self) -> &'static str; @@ -30,6 +56,12 @@ pub trait Service: Send + Sync { fn labels(&self) -> HashMap { HashMap::new() } fn cap_add(&self) -> Vec { vec![] } fn sysctls(&self) -> Vec { vec![] } + + /// Returns resource limits/reservations based on hardware + fn resources(&self, _hw: &HardwareInfo) -> Option { None } + + /// Returns logging configuration + fn logging(&self) -> LoggingConfig { LoggingConfig::default() } } pub fn get_all_services() -> Vec> { diff --git a/server_manager/tests/integration_tests.rs b/server_manager/tests/integration_tests.rs index d3e4578..2871da0 100644 --- a/server_manager/tests/integration_tests.rs +++ b/server_manager/tests/integration_tests.rs @@ -103,3 +103,34 @@ fn test_profile_logic_standard() { let has_enabled_spam = envs.iter().any(|v| v.as_str().unwrap() == "ENABLE_SPAMASSASSIN=1"); assert!(has_enabled_spam, "Standard profile should enable SpamAssassin"); } + +#[test] +fn test_resource_generation() { + // Test that resources are generated correctly for MariaDB on High Profile + let hw = HardwareInfo { + profile: HardwareProfile::High, + ram_gb: 32, + cpu_cores: 16, + has_nvidia: false, + has_intel_quicksync: false, + disk_gb: 1000, + swap_gb: 4, + }; + let secrets = Secrets::default(); + + let result = build_compose_structure(&hw, &secrets).unwrap(); + let services = result.get(&serde_yaml::Value::from("services")).unwrap().as_mapping().unwrap(); + + // Check MariaDB + let mariadb = services.get(&serde_yaml::Value::from("mariadb")).unwrap().as_mapping().unwrap(); + + // Check deploy key exists + assert!(mariadb.contains_key(&serde_yaml::Value::from("deploy"))); + + let deploy = mariadb.get(&serde_yaml::Value::from("deploy")).unwrap().as_mapping().unwrap(); + let resources = deploy.get(&serde_yaml::Value::from("resources")).unwrap().as_mapping().unwrap(); + let limits = resources.get(&serde_yaml::Value::from("limits")).unwrap().as_mapping().unwrap(); + + let memory = limits.get(&serde_yaml::Value::from("memory")).unwrap().as_str().unwrap(); + assert_eq!(memory, "4G", "MariaDB should have 4G limit on High profile"); +}