This commit is contained in:
rchac committed 2026-04-14 22:55:25 +00:00
1 parent 2a4478de34
commit db56ab135f
5 files changed
+130 -34

No files matched your search

+4
View File
@@ -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)
}
+67
View File
@@ -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<String>,
pub node_name: String,
pub config_load_error: Option<String>,
}
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<String> {
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<String> {
existing_config_load_error_for_path(&current_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<Mutex<()>> = 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");
}
}
+4 -19
View File
@@ -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();
}),
);
+45 -15
View File
@@ -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<Mutex<PendingChildren>> =
Lazy::new(|| Mutex::new(PendingChildren::default()));
@@ -236,20 +235,51 @@ fn load_existing_or_default(event_log: &mut Vec<String>) -> Result<lqos_config::
return Ok((*config).clone());
}
let config_path = Path::new("/etc/lqos.conf");
if config_path.exists() {
let backup_path = "/etc/lqos.conf.setupbackup";
std::fs::copy(config_path, backup_path).with_context(|| {
format!(
"Existing /etc/lqos.conf could not be loaded and could not be backed up to {backup_path}"
)
})?;
event_log.push(format!(
"Existing /etc/lqos.conf could not be loaded. Backup saved to {backup_path}."
));
} else {
event_log.push("Creating new configuration".to_string());
if let Some(message) = existing_config_load_error() {
bail!("{message}");
}
event_log.push("Creating new configuration".to_string());
Ok(lqos_config::Config::default())
}
#[cfg(test)]
mod tests {
use super::load_existing_or_default;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
static CONFIG_ENV_LOCK: Lazy<Mutex<()>> = 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");
}
}
+10
View File
@@ -183,6 +183,9 @@ async fn save_setup(Form(form): Form<SaveForm>) -> 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("<label>Confirm Password<input type=\"password\" name=\"confirm_password\" required></label>");
html.push_str("<button type=\"submit\">Create Admin</button></form></section>");
} else if let Some(config_error) = config.config_load_error.as_ref() {
html.push_str("<section class=\"card\"><h2>Existing Config Needs Repair</h2>");
html.push_str(&format!(
"<p class=\"notice\">{}</p>",
escape_html(config_error)
));
html.push_str("<p class=\"muted\">LibreQoS will not save setup changes until the existing configuration file can be loaded again.</p></section>");
} else if !snapshot.hotfix_required {
html.push_str("<section class=\"card\"><h2>Core Setup</h2>");
html.push_str("<form id=\"coreSetupForm\" method=\"post\" action=\"/setup/save\" onsubmit=\"return validateAndSubmitSetup(this);\">");