diff --git a/src/rust/lqos_bakery/src/lib.rs b/src/rust/lqos_bakery/src/lib.rs index 59a522c8..5eec56f9 100644 --- a/src/rust/lqos_bakery/src/lib.rs +++ b/src/rust/lqos_bakery/src/lib.rs @@ -10771,6 +10771,10 @@ mod tests { let dir = std::env::temp_dir().join(format!("lqos-bakery-{name}-{ts}")); std::fs::create_dir_all(&dir).expect("temp runtime dir"); cfg.lqos_directory = dir.display().to_string(); + // Keep Bakery qdisc-handle state under the per-test runtime directory so + // these allocator tests do not share `/opt/libreqos/state` across + // parallel test threads. + cfg.state_directory = None; Arc::new(cfg) } diff --git a/src/rust/lqos_setup/src/config_builder.rs b/src/rust/lqos_setup/src/config_builder.rs index 7d15eab4..fb7b4cca 100644 --- a/src/rust/lqos_setup/src/config_builder.rs +++ b/src/rust/lqos_setup/src/config_builder.rs @@ -1,3 +1,5 @@ +use std::path::{Path, PathBuf}; + use parking_lot::Mutex; use once_cell::sync::Lazy; @@ -24,10 +26,12 @@ pub struct ConfigBuilder { pub mbps_to_network: u64, pub allow_subnets: Vec, pub node_name: String, + pub config_load_error: Option, } impl ConfigBuilder { pub fn new() -> Self { + let config_path = current_config_path(); if let Ok(cfg) = lqos_config::load_config() { let mut to_internet = String::new(); let mut to_network = String::new(); @@ -61,8 +65,10 @@ impl ConfigBuilder { mbps_to_network: cfg.queues.uplink_bandwidth_mbps, allow_subnets: cfg.ip_ranges.allow_subnets.clone(), node_name: cfg.node_name.clone(), + config_load_error: None, } } else { + let config_load_error = existing_config_load_error_for_path(&config_path); // Default configuration if no config is loaded ConfigBuilder { bridge_mode: BridgeMode::Linux, @@ -79,14 +85,75 @@ impl ConfigBuilder { "192.168.0.0/16".to_string(), ], node_name: "LibreQoS".to_string(), + config_load_error, } } } } +pub fn current_config_path() -> PathBuf { + std::env::var_os("LQOS_CONFIG") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/etc/lqos.conf")) +} + +fn existing_config_load_error_for_path(config_path: &Path) -> Option { + config_path.exists().then(|| { + format!( + "Existing LibreQoS configuration at {} could not be loaded. Fix or replace it before setup can continue.", + config_path.display() + ) + }) +} + +pub fn existing_config_load_error() -> Option { + existing_config_load_error_for_path(¤t_config_path()) +} + pub fn existing_config_uses_xdp() -> bool { lqos_config::load_config() .ok() .and_then(|config| config.bridge.as_ref().map(|bridge| bridge.use_xdp_bridge)) .unwrap_or(false) } + +#[cfg(test)] +mod tests { + use super::ConfigBuilder; + use once_cell::sync::Lazy; + use parking_lot::Mutex; + use std::fs; + use std::time::{SystemTime, UNIX_EPOCH}; + + static CONFIG_ENV_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); + + #[test] + fn invalid_existing_config_sets_blocking_load_error() { + let _guard = CONFIG_ENV_LOCK.lock(); + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock before epoch") + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "libreqos-setup-invalid-config-{}-{unique}.toml", + std::process::id() + )); + fs::write(&path, "not valid toml = [\n").expect("write invalid config"); + let old_lqos_config = std::env::var_os("LQOS_CONFIG"); + unsafe { + std::env::set_var("LQOS_CONFIG", &path); + } + lqos_config::clear_cached_config(); + + let builder = ConfigBuilder::new(); + + assert!(builder.config_load_error.is_some()); + + match old_lqos_config { + Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) }, + None => unsafe { std::env::remove_var("LQOS_CONFIG") }, + } + lqos_config::clear_cached_config(); + fs::remove_file(path).expect("remove temp config"); + } +} diff --git a/src/rust/lqos_setup/src/main.rs b/src/rust/lqos_setup/src/main.rs index dbbca920..6cff7dcb 100644 --- a/src/rust/lqos_setup/src/main.rs +++ b/src/rust/lqos_setup/src/main.rs @@ -14,7 +14,7 @@ use lqos_setup::{bootstrap, hotfix}; use std::path::Path; use bandwidth::bandwidth_view; -use config_builder::CURRENT_CONFIG; +use config_builder::{CURRENT_CONFIG, existing_config_load_error}; use cursive::{ Rect, Vec2, View, view::{Margins, Nameable, Resizable, Scrollable}, @@ -353,26 +353,11 @@ fn finalize(ui: &mut cursive::Cursive) { } } - // If we cannot load the config but a file exists, warn the user that - // continuing will replace it after saving a backup. - let config_path = Path::new("/etc/lqos.conf"); - let load_result = lqos_config::load_config(); - if load_result.is_err() && config_path.exists() { - let msg = "An existing configuration file was found at /etc/lqos.conf,\n\ -but it could not be read or parsed.\n\n\ -Press Continue to replace it with a new configuration using defaults.\n\ -LibreQoS will first try to save a backup to /etc/lqos.conf.setupbackup.\n\n\ -Press Cancel to exit and investigate." - .to_string(); - + if let Some(message) = existing_config_load_error() { ui.add_layer( - Dialog::around(TextView::new(msg)) + Dialog::around(TextView::new(message)) .title("Existing Config Unreadable") - .button("Continue", |s| { - s.pop_layer(); - continue_finalize(s); - }) - .button("Cancel", |s| { + .button("OK", |s| { s.pop_layer(); }), ); diff --git a/src/rust/lqos_setup/src/setup_actions.rs b/src/rust/lqos_setup/src/setup_actions.rs index 44b1dec7..b27c659a 100644 --- a/src/rust/lqos_setup/src/setup_actions.rs +++ b/src/rust/lqos_setup/src/setup_actions.rs @@ -1,6 +1,6 @@ //! Shared setup commit/apply helpers used by both Cursive and the setup WebUI. -use crate::config_builder::{BridgeMode, CURRENT_CONFIG}; +use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_load_error}; use anyhow::{Context, Result, bail}; use lqos_netplan_helper::protocol::{ApplyMode, ApplyRequest}; use lqos_netplan_helper::transaction::{ @@ -11,7 +11,6 @@ use once_cell::sync::Lazy; use parking_lot::Mutex; use std::collections::HashMap; use std::fmt::Write as _; -use std::path::Path; static PENDING_CHILDREN: Lazy> = Lazy::new(|| Mutex::new(PendingChildren::default())); @@ -236,20 +235,51 @@ fn load_existing_or_default(event_log: &mut Vec) -> Result> = Lazy::new(|| Mutex::new(())); + + #[test] + fn existing_unreadable_config_blocks_default_fallback() { + let _guard = CONFIG_ENV_LOCK.lock(); + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock before epoch") + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "libreqos-setup-actions-invalid-config-{}-{unique}.toml", + std::process::id() + )); + fs::write(&path, "not valid toml = [\n").expect("write invalid config"); + let old_lqos_config = std::env::var_os("LQOS_CONFIG"); + unsafe { + std::env::set_var("LQOS_CONFIG", &path); + } + lqos_config::clear_cached_config(); + + let error = load_existing_or_default(&mut Vec::new()).expect_err("should block"); + + assert!(error.to_string().contains("could not be loaded")); + + match old_lqos_config { + Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) }, + None => unsafe { std::env::remove_var("LQOS_CONFIG") }, + } + lqos_config::clear_cached_config(); + fs::remove_file(path).expect("remove temp config"); + } +} diff --git a/src/rust/lqos_setup/src/web.rs b/src/rust/lqos_setup/src/web.rs index eea8cf4a..e2d5f39a 100644 --- a/src/rust/lqos_setup/src/web.rs +++ b/src/rust/lqos_setup/src/web.rs @@ -183,6 +183,9 @@ async fn save_setup(Form(form): Form) -> Response { "Create the first admin account before saving setup settings.", ); } + if let Some(message) = CURRENT_CONFIG.lock().config_load_error.clone() { + return error_response(StatusCode::BAD_REQUEST, &message); + } if let Err(err) = apply_form_to_current_config(&form) { return error_response(StatusCode::BAD_REQUEST, &err.to_string()); } @@ -313,6 +316,13 @@ fn render_setup_page( ); html.push_str(""); html.push_str(""); + } else if let Some(config_error) = config.config_load_error.as_ref() { + html.push_str("

Existing Config Needs Repair

"); + html.push_str(&format!( + "

{}

", + escape_html(config_error) + )); + html.push_str("

LibreQoS will not save setup changes until the existing configuration file can be loaded again.

"); } else if !snapshot.hotfix_required { html.push_str("

Core Setup

"); html.push_str("
");