Skip to content
Merged
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
18 changes: 15 additions & 3 deletions server_manager/src/core/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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")?;
Expand Down
48 changes: 48 additions & 0 deletions server_manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
15 changes: 14 additions & 1 deletion server_manager/src/services/arr.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -30,6 +30,19 @@ macro_rules! define_arr_service {
fn healthcheck(&self) -> Option<String> {
Some(format!("curl -f http://localhost:{}/ping || exit 1", $port))
}

fn resources(&self, hw: &HardwareInfo) -> Option<ResourceConfig> {
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,
})
}
}
};
}
Expand Down
35 changes: 33 additions & 2 deletions server_manager/src/services/infra.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -62,13 +62,44 @@ impl Service for MariaDBService {
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/mariadb:/config".to_string()]
}
fn networks(&self) -> Vec<String> { vec!["server_manager_net".to_string()] }

fn resources(&self, hw: &HardwareInfo) -> Option<ResourceConfig> {
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;
impl Service for RedisService {
fn name(&self) -> &'static str { "redis" }
fn image(&self) -> &'static str { "redis:alpine" }
fn ports(&self) -> Vec<String> { vec!["6379:6379".to_string()] }
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/redis:/data".to_string()]
}
fn resources(&self, hw: &HardwareInfo) -> Option<ResourceConfig> {
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;
Expand Down
32 changes: 31 additions & 1 deletion server_manager/src/services/media.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -56,6 +56,20 @@ impl Service for PlexService {
fn healthcheck(&self) -> Option<String> {
Some("curl -f http://localhost:32400/identity || exit 1".to_string())
}

fn resources(&self, hw: &HardwareInfo) -> Option<ResourceConfig> {
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;
Expand All @@ -67,6 +81,14 @@ impl Service for TautulliService {
vec!["./config/tautulli:/config".to_string()]
}
fn depends_on(&self) -> Vec<String> { vec!["plex".to_string()] }
fn resources(&self, _hw: &HardwareInfo) -> Option<ResourceConfig> {
Some(ResourceConfig {
memory_limit: Some("512M".to_string()),
memory_reservation: None,
cpu_limit: None,
cpu_reservation: None,
})
}
}

pub struct OverseerrService;
Expand All @@ -77,4 +99,12 @@ impl Service for OverseerrService {
fn volumes(&self, _hw: &HardwareInfo) -> Vec<String> {
vec!["./config/overseerr:/config".to_string()]
}
fn resources(&self, _hw: &HardwareInfo) -> Option<ResourceConfig> {
Some(ResourceConfig {
memory_limit: Some("1G".to_string()),
memory_reservation: None,
cpu_limit: None,
cpu_reservation: None,
})
}
}
32 changes: 32 additions & 0 deletions server_manager/src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub memory_reservation: Option<String>,
pub cpu_limit: Option<String>,
pub cpu_reservation: Option<String>,
}

#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub driver: String,
pub options: HashMap<String, String>,
}

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;
Expand All @@ -30,6 +56,12 @@ pub trait Service: Send + Sync {
fn labels(&self) -> HashMap<String, String> { HashMap::new() }
fn cap_add(&self) -> Vec<String> { vec![] }
fn sysctls(&self) -> Vec<String> { vec![] }

/// Returns resource limits/reservations based on hardware
fn resources(&self, _hw: &HardwareInfo) -> Option<ResourceConfig> { None }

/// Returns logging configuration
fn logging(&self) -> LoggingConfig { LoggingConfig::default() }
}

pub fn get_all_services() -> Vec<Box<dyn Service>> {
Expand Down
31 changes: 31 additions & 0 deletions server_manager/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Loading