This commit is contained in:
rchac committed 2026-04-14 22:35:25 +00:00
1 parent e0c665878d
commit 56b3bebc52
13 files changed
+273 -123

No files matched your search

+4
View File
@@ -15,6 +15,10 @@ A continuación, se encuentran las instrucciones para configurar Netplan, ya sea
La página Network Mode en la interfaz web de LibreQoS ahora inspecciona los archivos actuales de Netplan, ofrece en menús desplegables las interfaces elegibles que no forman parte de la ruta de gestión, prepara cambios administrados para `libreqos.yaml` en los modos puente de Linux y de interfaz única, los aplica con una ventana temporizada de reversión manejada por LibreQoS y le permite confirmar o revertir el cambio pendiente. También puede restaurar desde esa página la copia de seguridad administrada anterior. El modo puente XDP sigue siendo un flujo manual de Netplan.
```
```{note}
La configuración inicial actual solo ofrece Puente Linux e Interfaz Única para instalaciones nuevas. Si LibreQoS detecta un despliegue XDP existente, la configuración preserva ese modo XDP heredado y le avisa antes de migrar fuera de él.
```
```{note}
Si un cambio temporizado de Netplan interrumpe brevemente la sesión del navegador, vuelva a la página Network Mode cuando regrese la conectividad. LibreQoS retomará automáticamente desde esa página el flujo pendiente de confirmar o revertir.
```
+4
View File
@@ -15,6 +15,10 @@ Below are the instructions to configure Netplan, whether using the Linux Bridge
The Network Mode page in the LibreQoS web UI now inspects the current Netplan files, offers eligible non-management interfaces in dropdowns, stages managed `libreqos.yaml` changes for Linux bridge and single-interface modes, applies them with a timed LibreQoS rollback window, and lets you confirm or revert the pending change. You can also restore the previous managed backup from that page. XDP bridge mode remains a manual Netplan workflow.
```
```{note}
Current first-run setup only offers Linux Bridge and Single Interface for new installs. If LibreQoS detects an existing XDP deployment, setup preserves that legacy XDP mode and warns before you migrate away from it.
```
```{note}
If a timed Netplan change briefly interrupts your browser session, return to the Network Mode page after connectivity comes back. LibreQoS will resume the pending confirm or revert flow from that page automatically.
```
+26 -18
View File
@@ -140,16 +140,6 @@ else
fi
}
should_activate_runtime_on_upgrade() {
local previous_version="${2:-}"
# Debian passes the previous package version as the second postinst argument on
# upgrade. If an existing LibreQoS config is already present, keep runtime
# services in charge of the main WebUI and let lqosd handle any follow-up
# onboarding instead of launching the dedicated setup web service on port 9123.
[ -n "$previous_version" ] && [ -f /etc/lqos.conf ]
}
# Install Python Dependencies
pushd /opt/libreqos > /dev/null
# - Setup Python dependencies as a post-install task. Use --ignore-installed so
@@ -175,14 +165,32 @@ install -m 0644 /opt/libreqos/src/bin/lqos_setup.service.example /etc/systemd/sy
/bin/systemctl disable lqos_node_manager || true # In case it's running from a previous release
/bin/systemctl stop lqos_netplan_helper || true
/bin/systemctl disable lqos_netplan_helper || true
if should_activate_runtime_on_upgrade "$@"; then
/opt/libreqos/src/bin/lqos_setup activate-runtime
elif /opt/libreqos/src/bin/lqos_setup is-ready; then
/opt/libreqos/src/bin/lqos_setup activate-runtime
else
/opt/libreqos/src/bin/lqos_setup activate-setup
/opt/libreqos/src/bin/lqos_setup print-link || true
fi
case "$(/opt/libreqos/src/bin/lqos_setup postinst-action)" in
activate_runtime)
/opt/libreqos/src/bin/lqos_setup activate-runtime
;;
activate_setup)
/opt/libreqos/src/bin/lqos_setup activate-setup
/opt/libreqos/src/bin/lqos_setup print-link || true
;;
block_for_hotfix)
/opt/libreqos/src/bin/lqos_setup hotfix-status || true
cat <<'HOTFIX'
Install the Noble systemd hotfix before package configuration can start LibreQoS services.
Run:
sudo /opt/libreqos/src/systemd_hotfix.sh install
Then re-run the LibreQoS package installation.
HOTFIX
exit 1
;;
*)
echo "Unknown LibreQoS post-install action." >&2
exit 1
;;
esac
popd > /dev/null
EOF
+80 -1
View File
@@ -92,6 +92,28 @@ pub struct StatusSnapshot {
pub hotfix_detail: String,
}
/// Service mode the package postinst should activate after installation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PostinstAction {
/// Stop before starting services because the Noble systemd hotfix is still required.
BlockForHotfix,
/// Start the runtime services and keep setup inside `lqosd`.
ActivateRuntime,
/// Start the dedicated setup service for a true first-run install.
ActivateSetup,
}
impl PostinstAction {
/// Stable token printed for package postinst shell logic.
pub fn as_str(self) -> &'static str {
match self {
Self::BlockForHotfix => "block_for_hotfix",
Self::ActivateRuntime => "activate_runtime",
Self::ActivateSetup => "activate_setup",
}
}
}
#[derive(Default, Deserialize)]
struct AuthUsersFile {
#[serde(default)]
@@ -189,6 +211,24 @@ fn display_status(setup_complete: bool) -> SetupStatus {
}
}
fn legacy_runtime_install_detected() -> bool {
Path::new("/etc/lqos.conf").exists()
}
fn classify_postinst_action_from_inputs(
hotfix_required: bool,
runtime_ready: bool,
legacy_runtime_install: bool,
) -> PostinstAction {
if hotfix_required {
PostinstAction::BlockForHotfix
} else if runtime_ready || legacy_runtime_install {
PostinstAction::ActivateRuntime
} else {
PostinstAction::ActivateSetup
}
}
fn detect_host_hints() -> Vec<String> {
let mut hints = BTreeSet::new();
@@ -443,6 +483,18 @@ pub fn runtime_services_should_start() -> Result<bool> {
&& !hotfix_status.required)
}
/// Classifies which service mode package postinst should activate.
pub fn classify_postinst_action() -> Result<PostinstAction> {
let hotfix_required = hotfix::status()?.required;
let runtime_ready = runtime_services_should_start()?;
let legacy_runtime_install = legacy_runtime_install_detected();
Ok(classify_postinst_action_from_inputs(
hotfix_required,
runtime_ready,
legacy_runtime_install,
))
}
/// Returns true when first-run setup has not yet been completed.
pub fn setup_is_incomplete() -> Result<bool> {
let live = collect_live_inputs();
@@ -487,7 +539,10 @@ fn yes_no(value: bool) -> &'static str {
#[cfg(test)]
mod tests {
use super::{TOKEN_TTL, current_unix_seconds, new_bootstrap_token};
use super::{
PostinstAction, TOKEN_TTL, classify_postinst_action_from_inputs, current_unix_seconds,
new_bootstrap_token,
};
#[test]
fn bootstrap_tokens_expire_in_future() {
@@ -496,4 +551,28 @@ mod tests {
assert!(!token.token.is_empty());
assert!(token.expires_at_unix >= now + TOKEN_TTL.as_secs() - 1);
}
#[test]
fn postinst_action_blocks_for_hotfix_before_services_restart() {
assert_eq!(
classify_postinst_action_from_inputs(true, true, true),
PostinstAction::BlockForHotfix
);
}
#[test]
fn postinst_action_keeps_existing_runtime_installs_on_runtime_services() {
assert_eq!(
classify_postinst_action_from_inputs(false, false, true),
PostinstAction::ActivateRuntime
);
}
#[test]
fn postinst_action_only_activates_setup_for_true_first_run() {
assert_eq!(
classify_postinst_action_from_inputs(false, false, false),
PostinstAction::ActivateSetup
);
}
}
+30 -9
View File
@@ -4,10 +4,11 @@ use cursive::{
views::{Dialog, LinearLayout, RadioGroup, TextView},
};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp};
pub fn bridge_mode(s: &mut Cursive) {
let single_selected = CURRENT_CONFIG.lock().bridge_mode == BridgeMode::Single;
let current_mode = CURRENT_CONFIG.lock().bridge_mode;
let show_legacy_xdp = existing_config_uses_xdp() || current_mode == BridgeMode::XDP;
// create the group and buttons
let mut group = RadioGroup::new().on_change(|_s, mode| {
@@ -19,20 +20,40 @@ pub fn bridge_mode(s: &mut Cursive) {
BridgeMode::Linux,
"Linux Bridge (2 interfaces) - LibreQoS will inspect and stage the managed Netplan change",
);
let mut xdp_btn = show_legacy_xdp.then(|| {
group.button(
BridgeMode::XDP,
"Legacy XDP Bridge (existing installs only - keep only if you already run it)",
)
});
let mut single_btn = group.button(BridgeMode::Single, "Single Interface (1 interface)");
// mark the one we want as selected
if single_selected {
single_btn.select();
} else {
linux_btn.select();
match current_mode {
BridgeMode::Single => {
single_btn.select();
}
BridgeMode::XDP if show_legacy_xdp => {
if let Some(button) = xdp_btn.as_mut() {
button.select();
}
}
_ => {
linux_btn.select();
}
}
// now add them (in any order) to your layout
let layout = LinearLayout::vertical()
let mut layout = LinearLayout::vertical()
.child(TextView::new("Select the bridge mode you want to use:"))
.child(linux_btn)
.child(single_btn);
.child(linux_btn);
if let Some(button) = xdp_btn {
layout.add_child(TextView::new(
"Legacy XDP mode was detected on this install. New installs should use Linux Bridge; leave XDP selected only if you intend to keep the existing XDP deployment.",
));
layout.add_child(button);
}
layout.add_child(single_btn);
s.add_layer(
Dialog::around(layout)
@@ -83,3 +83,10 @@ impl ConfigBuilder {
}
}
}
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)
}
+2
View File
@@ -2,3 +2,5 @@
pub mod bootstrap;
pub mod hotfix;
pub use bootstrap::PostinstAction;
+9
View File
@@ -46,6 +46,8 @@ enum Command {
InstallHotfix,
/// Exit 0 when runtime services should start, or 1 when setup is still required
IsReady,
/// Internal helper: print the package postinst action token
PostinstAction,
/// Internal helper: stop setup and activate runtime services now
ActivateRuntime,
/// Internal helper: stop runtime and activate the first-run setup service now
@@ -213,6 +215,13 @@ fn main() {
std::process::exit(1);
}
},
Some(Command::PostinstAction) => match bootstrap::classify_postinst_action() {
Ok(action) => println!("{}", action.as_str()),
Err(err) => {
eprintln!("Unable to determine package postinst action: {err:#}");
std::process::exit(1);
}
},
Some(Command::ActivateRuntime) => match service_handoff::activate_runtime_services() {
Ok(message) => println!("{message}"),
Err(err) => {
+71 -31
View File
@@ -1,7 +1,7 @@
//! Minimal setup-only web server for first-run LibreQoS configuration.
use crate::{
config_builder::{BridgeMode, CURRENT_CONFIG},
config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp},
interfaces, service_handoff,
setup_actions::{self, CommitOutcome},
};
@@ -26,11 +26,6 @@ struct SetupQuery {
notice: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CompletedQuery {
auto: Option<String>,
}
#[derive(Debug, Deserialize)]
struct AdminForm {
token: String,
@@ -99,7 +94,7 @@ pub(crate) fn run() -> Result<()> {
async fn root() -> Redirect {
let target = match bootstrap::status_snapshot() {
Ok(snapshot) if snapshot.state.setup_complete => "/setup/completed?auto=1",
Ok(snapshot) if snapshot.state.setup_complete => "/setup/completed",
_ => "/setup",
};
Redirect::temporary(target)
@@ -112,9 +107,8 @@ async fn setup_page(Query(query): Query<SetupQuery>) -> Response {
}
}
async fn completed_page(Query(query): Query<CompletedQuery>) -> Response {
let automatic = query.auto.as_deref() == Some("1");
match render_completed_page(automatic) {
async fn completed_page() -> Response {
match render_completed_page() {
Ok(html) => Html(html).into_response(),
Err(err) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()),
}
@@ -248,8 +242,10 @@ fn render_setup_page(
let config = CURRENT_CONFIG.lock().clone();
let mode_label = match config.bridge_mode {
BridgeMode::Single => "single",
_ => "linux",
BridgeMode::XDP => "xdp",
BridgeMode::Linux => "linux",
};
let persisted_xdp = existing_config_uses_xdp();
let mut html = page_shell("LibreQoS Setup");
html.push_str("<main class=\"setup-shell\">");
@@ -285,6 +281,16 @@ fn render_setup_page(
));
html.push_str("</ul></section>");
if persisted_xdp {
html.push_str("<section class=\"card\"><h2>Legacy XDP Mode Detected</h2>");
if config.bridge_mode == BridgeMode::XDP {
html.push_str("<p class=\"notice\">This system already uses legacy XDP bridge mode. LibreQoS no longer recommends XDP for new installs, but this page will preserve it if you leave the mode unchanged.</p>");
} else {
html.push_str("<p class=\"notice\">This system previously used legacy XDP bridge mode. Saving with Linux Bridge or Single Interface will migrate the node away from XDP.</p>");
}
html.push_str("<p class=\"muted\">Use Linux Bridge for long-term maintenance unless you intentionally need to keep the existing XDP deployment.</p></section>");
}
if snapshot.hotfix_required {
html.push_str("<section class=\"card\"><h2>Ubuntu 24.04 Hotfix Required</h2>");
html.push_str(&format!(
@@ -328,13 +334,19 @@ fn render_setup_page(
"<option value=\"linux\"{}>Linux Bridge</option>",
selected(mode_label == "linux")
));
if persisted_xdp {
html.push_str(&format!(
"<option value=\"xdp\"{}>Legacy XDP Bridge (existing installs only)</option>",
selected(mode_label == "xdp")
));
}
html.push_str(&format!(
"<option value=\"single\"{}>Single Interface</option>",
selected(mode_label == "single")
));
html.push_str("</select></label>");
html.push_str("<div class=\"mode-section\" data-mode=\"linux\">");
html.push_str("<p class=\"muted\">Recommended for most installs. Select the WAN-facing and subscriber-facing interfaces.</p>");
html.push_str("<div class=\"mode-section\" data-mode-section=\"bridge\">");
html.push_str("<p class=\"muted\">Linux Bridge is recommended for most installs. Select the WAN-facing and subscriber-facing interfaces below. Existing XDP installs use the same interface pair if you choose to preserve XDP.</p>");
html.push_str("<label>To Internet<select name=\"to_internet\">");
for iface in &interface_options {
html.push_str(&format!(
@@ -357,7 +369,7 @@ fn render_setup_page(
html.push_str("</select></label>");
html.push_str("<p id=\"interfaceValidationMessage\" class=\"validation-message\" hidden>Internet and network interfaces must be different.</p>");
html.push_str("</div>");
html.push_str("<div class=\"mode-section\" data-mode=\"single\">");
html.push_str("<div class=\"mode-section\" data-mode-section=\"single\">");
html.push_str("<p class=\"muted\">Use this when a single interface carries both internet and subscriber traffic with optional VLAN tags.</p>");
html.push_str("<label>Interface<select name=\"single_interface\">");
for iface in &interface_options {
@@ -420,7 +432,8 @@ fn apply_form_to_current_config(form: &SaveForm) -> Result<()> {
config.mbps_to_internet = downlink;
config.mbps_to_network = uplink;
config.allow_subnets = allow_subnets;
match form.bridge_mode.as_str() {
let allow_legacy_xdp = config.bridge_mode == BridgeMode::XDP || existing_config_uses_xdp();
match resolve_requested_bridge_mode(&form.bridge_mode, allow_legacy_xdp)? {
"single" => {
config.bridge_mode = BridgeMode::Single;
let interface = form
@@ -434,7 +447,7 @@ fn apply_form_to_current_config(form: &SaveForm) -> Result<()> {
config.internet_vlan = parse_vlan(form.internet_vlan.as_deref())?;
config.network_vlan = parse_vlan(form.network_vlan.as_deref())?;
}
"linux" => {
"linux" | "xdp" => {
let to_network = form
.to_network
.as_deref()
@@ -444,7 +457,11 @@ fn apply_form_to_current_config(form: &SaveForm) -> Result<()> {
if form.to_internet.trim() == to_network {
bail!("Internet and network interfaces must be different.");
}
config.bridge_mode = BridgeMode::Linux;
config.bridge_mode = if form.bridge_mode == "xdp" {
BridgeMode::XDP
} else {
BridgeMode::Linux
};
config.to_internet = form.to_internet.trim().to_string();
config.to_network = to_network.to_string();
config.internet_vlan = 0;
@@ -465,6 +482,14 @@ fn parse_vlan(raw: Option<&str>) -> Result<u32> {
.with_context(|| format!("Invalid VLAN value: {value}"))
}
fn resolve_requested_bridge_mode(requested_mode: &str, allow_legacy_xdp: bool) -> Result<&str> {
match requested_mode {
"single" | "linux" => Ok(requested_mode),
"xdp" if allow_legacy_xdp => Ok("xdp"),
_ => bail!("Unsupported bridge mode."),
}
}
fn ensure_valid_setup_token(token: &str) -> Result<()> {
if bootstrap::validate_setup_token(token)? {
Ok(())
@@ -544,16 +569,16 @@ fn finalize_success_html(
.as_ref()
.is_some_and(|notice| notice.automatic)
{
html.push_str("<p class=\"muted\">This setup page may stop while LibreQoS runtime services start. Refresh this address after the handoff if you want to load the normal runtime UI.</p>");
html.push_str(
"<script>if(window.history&&window.history.replaceState){window.history.replaceState({},'', '/');}setTimeout(function(){window.location.replace('/');},1500);</script>",
);
html.push_str("<p class=\"muted\">LibreQoS runtime services are starting now. This setup page may stop responding during the handoff. Use the link below after the handoff if you want to load the normal runtime UI.</p>");
}
html.push_str(
"<div class=\"button-row\"><a class=\"button-link\" href=\"/\">Open LibreQoS</a></div>",
);
html.push_str("</section></main></body></html>");
Ok(html)
}
fn render_completed_page(automatic: bool) -> Result<String> {
fn render_completed_page() -> Result<String> {
let snapshot = bootstrap::status_snapshot()?;
let report = bootstrap::load_setup_completion_report().unwrap_or_default();
let mut html = page_shell("Setup Complete");
@@ -568,12 +593,10 @@ fn render_completed_page(automatic: bool) -> Result<String> {
escape_html(&report)
));
}
if automatic {
html.push_str("<p class=\"muted\">This setup page may stop while LibreQoS runtime services start. Refresh this address after the handoff if you want to load the normal runtime UI.</p>");
html.push_str(
"<script>setTimeout(function(){window.location.replace('/');},1500);</script>",
);
}
html.push_str("<p class=\"muted\">Use the button below after the handoff if you want to load the normal runtime UI.</p>");
html.push_str(
"<div class=\"button-row\"><a class=\"button-link\" href=\"/\">Open LibreQoS</a></div>",
);
html.push_str("</section></main></body></html>");
Ok(html)
}
@@ -605,7 +628,7 @@ fn base_css() -> &'static str {
input:focus,select:focus,textarea:focus{outline:none;border-color:rgba(37,99,235,.45);box-shadow:0 0 0 4px rgba(59,130,246,.16)}\
button{margin-top:16px;background:linear-gradient(135deg,var(--lqos-primary),var(--lqos-accent));color:#fff;border:0;border-radius:999px;padding:11px 18px;font:inherit;font-weight:800;cursor:pointer;box-shadow:0 12px 24px rgba(37,99,235,.18)}\
button[disabled]{opacity:.7;cursor:progress}\
button.secondary{background:linear-gradient(135deg,#64748b,#475569)}.button-row{display:flex;gap:12px;flex-wrap:wrap}.button-row form{margin:0}\
button.secondary{background:linear-gradient(135deg,#64748b,#475569)}.button-row{display:flex;gap:12px;flex-wrap:wrap}.button-row form{margin:0}.button-link{display:inline-flex;align-items:center;justify-content:center;text-decoration:none;background:linear-gradient(135deg,var(--lqos-primary),var(--lqos-accent));color:#fff;border-radius:999px;padding:11px 18px;font-weight:800;box-shadow:0 12px 24px rgba(37,99,235,.18)}\
.muted{color:var(--lqos-muted)}.notice{background:#fff7d6;border:1px solid #e7d48b;padding:12px 14px;border-radius:12px}\
.notice-details{margin-top:10px}.notice-details summary{cursor:pointer;font-weight:700}\
.mode-section[hidden]{display:none}\
@@ -620,14 +643,15 @@ fn base_css() -> &'static str {
}
fn base_js() -> &'static str {
"function syncModeSections(){const mode=document.querySelector('select[name=\"bridge_mode\"]')?.value||'linux';document.querySelectorAll('.mode-section').forEach((section)=>{section.hidden=section.dataset.mode!==mode;});updateCoreSetupValidation();}\
"function bridgeSectionForMode(mode){return mode==='single'?'single':'bridge';}\
function syncModeSections(){const mode=document.querySelector('select[name=\"bridge_mode\"]')?.value||'linux';const visibleSection=bridgeSectionForMode(mode);document.querySelectorAll('.mode-section').forEach((section)=>{section.hidden=section.dataset.modeSection!==visibleSection;});updateCoreSetupValidation();}\
function showBusy(message){const overlay=document.getElementById('busyOverlay');const target=document.getElementById('busyMessage');if(target&&message){target.textContent=message;}if(overlay){overlay.hidden=false;}}\
function hideBusy(){const overlay=document.getElementById('busyOverlay');if(overlay){overlay.hidden=true;}}\
function setSubmitDisabled(form,disabled){const button=form?.querySelector('button[type=\"submit\"]');if(button){button.disabled=!!disabled;}}\
function submitWithProgress(form,message){setSubmitDisabled(form,true);showBusy(message);return true;}\
function currentBridgeMode(){return document.querySelector('select[name=\"bridge_mode\"]')?.value||'linux';}\
function linuxInterfacesAreDistinct(){const toInternet=document.querySelector('select[name=\"to_internet\"]')?.value||'';const toNetwork=document.querySelector('select[name=\"to_network\"]')?.value||'';return !toInternet||!toNetwork||toInternet!==toNetwork;}\
function updateCoreSetupValidation(){const form=document.getElementById('coreSetupForm');if(!form){return true;}const message=document.getElementById('interfaceValidationMessage');const invalid=currentBridgeMode()==='linux'&&!linuxInterfacesAreDistinct();if(message){message.hidden=!invalid;}setSubmitDisabled(form,invalid);return !invalid;}\
function updateCoreSetupValidation(){const form=document.getElementById('coreSetupForm');if(!form){return true;}const message=document.getElementById('interfaceValidationMessage');const invalid=currentBridgeMode()!=='single'&&!linuxInterfacesAreDistinct();if(message){message.hidden=!invalid;}setSubmitDisabled(form,invalid);return !invalid;}\
function validateAndSubmitSetup(form){if(!updateCoreSetupValidation()){hideBusy();setSubmitDisabled(form,false);return false;}return submitWithProgress(form,'Saving setup and applying managed network changes...');}\
function resetBusyState(){hideBusy();document.querySelectorAll('form').forEach((form)=>setSubmitDisabled(form,false));updateCoreSetupValidation();}\
function setupPage(){const mode=document.querySelector('select[name=\"bridge_mode\"]');if(mode){mode.addEventListener('change',syncModeSections);}const toInternet=document.querySelector('select[name=\"to_internet\"]');if(toInternet){toInternet.addEventListener('change',updateCoreSetupValidation);}const toNetwork=document.querySelector('select[name=\"to_network\"]');if(toNetwork){toNetwork.addEventListener('change',updateCoreSetupValidation);}window.addEventListener('pageshow',resetBusyState);syncModeSections();resetBusyState();}"
@@ -657,3 +681,19 @@ fn yes_no(value: bool) -> &'static str {
fn selected(value: bool) -> &'static str {
if value { " selected" } else { "" }
}
#[cfg(test)]
mod tests {
use super::resolve_requested_bridge_mode;
#[test]
fn xdp_mode_is_preserved_for_existing_xdp_config() {
assert_eq!(resolve_requested_bridge_mode("xdp", true).unwrap(), "xdp");
}
#[test]
fn xdp_mode_is_not_available_for_new_installs() {
let error = resolve_requested_bridge_mode("xdp", false).unwrap_err();
assert!(error.to_string().contains("Unsupported bridge mode"));
}
}
@@ -6,17 +6,6 @@ function setText(id, text) {
element.textContent = text;
}
function syncIntegrationAction() {
const button = document.getElementById("btnOpenIntegrationProvider");
const select = document.getElementById("runtimeIntegrationProvider");
if (!button || !select) return;
button.disabled = !select.value;
select.addEventListener("change", () => {
button.disabled = !select.value;
});
}
function setStatusAlert(state) {
const element = document.getElementById("runtimeSetupStatus");
if (!element) return;
@@ -48,28 +37,14 @@ function renderState(state) {
);
setText("runtimeNetworkJson", state?.network_json_present ? "Present" : "Missing");
setText("runtimeShapedDevices", state?.shaped_devices_present ? "Present" : "Missing");
syncIntegrationAction();
}
function initActions() {
const button = document.getElementById("btnOpenIntegrationProvider");
const select = document.getElementById("runtimeIntegrationProvider");
if (!button || !select) return;
button.addEventListener("click", () => {
if (button.disabled) return;
window.location.href = select.value;
});
}
loadConfig((msg) => {
renderState(msg?.data?.runtime_onboarding || {});
initActions();
}, () => {
setStatusAlert({
required: true,
status_label: "Setup Required",
summary: "Unable to load runtime setup status right now.",
});
syncIntegrationAction();
initActions();
});
@@ -400,8 +400,7 @@ fn inspect_png(body: &[u8]) -> Result<(u32, u32), CobrandUploadValidationError>
}
fn rendered_cobrand_width_exceeds_sidebar(width: u32, height: u32) -> bool {
u64::from(width) * COBRAND_DISPLAY_HEIGHT_PX
> u64::from(height) * COBRAND_MAX_DISPLAY_WIDTH_PX
u64::from(width) * COBRAND_DISPLAY_HEIGHT_PX > u64::from(height) * COBRAND_MAX_DISPLAY_WIDTH_PX
}
fn persist_cobrand_png(body: &[u8]) -> Result<(), StatusCode> {
@@ -411,10 +410,7 @@ fn persist_cobrand_png(body: &[u8]) -> Result<(), StatusCode> {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
std::fs::create_dir_all(parent_dir).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let temp_root = parent_dir
.parent()
.unwrap_or(parent_dir)
.join(".tmp");
let temp_root = parent_dir.parent().unwrap_or(parent_dir).join(".tmp");
std::fs::create_dir_all(&temp_root).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut temp_path = None;
for attempt in 0..32 {
@@ -482,8 +478,12 @@ pub async fn upload_cobrand(
"Cobrand image is too wide for the sidebar at 48px tall. Keep the rendered width at or below 176px.",
),
})?;
persist_cobrand_png(&body)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Unable to save cobrand.png."))?;
persist_cobrand_png(&body).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Unable to save cobrand.png.",
)
})?;
Ok(StatusCode::NO_CONTENT)
}
@@ -1023,9 +1023,9 @@ pub struct UserRequest {
#[cfg(test)]
mod tests {
use super::{
CobrandUploadValidationError, ConfigSecretClearRequest, apply_secret_updates,
cobrand_path, persist_cobrand_png, redact_config_secrets, upload_cobrand,
validate_cobrand_upload, validate_network_json,
CobrandUploadValidationError, ConfigSecretClearRequest, apply_secret_updates, cobrand_path,
persist_cobrand_png, redact_config_secrets, upload_cobrand, validate_cobrand_upload,
validate_network_json,
};
use crate::node_manager::auth::LoginResult;
use crate::test_support::runtime_config_test_lock;
@@ -1034,16 +1034,16 @@ mod tests {
use axum::http::HeaderMap;
use lqos_config::Config;
use serde_json::json;
use std::fs;
use std::ffi::OsString;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const VALID_PNG: &[u8] = &[
0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H',
b'D', b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78,
0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, b'I', b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82,
0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H', b'D',
b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78, 0x9C, 0x63, 0x00,
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, b'I',
b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82,
];
fn encode_test_png(width: u32, height: u32) -> Vec<u8> {
@@ -1357,8 +1357,14 @@ mod tests {
fn validate_cobrand_upload_accepts_boundary_dimensions() {
let max_width = encode_test_png(176, 48);
let max_height = encode_test_png(1, 4096);
assert_eq!(validate_cobrand_upload(Some("image/png"), &max_width), Ok(()));
assert_eq!(validate_cobrand_upload(Some("image/png"), &max_height), Ok(()));
assert_eq!(
validate_cobrand_upload(Some("image/png"), &max_width),
Ok(())
);
assert_eq!(
validate_cobrand_upload(Some("image/png"), &max_height),
Ok(())
);
}
#[tokio::test]
@@ -23,24 +23,19 @@
<p class="text-body-secondary">
Choose the system that already owns your subscribers and let LibreQoS reuse its existing integration page.
</p>
<label for="runtimeIntegrationProvider" class="form-label mt-2">Integration Provider</label>
<select class="form-select mb-3" id="runtimeIntegrationProvider">
<option value="" disabled selected>Choose provider</option>
<option value="config_uisp.html">UISP</option>
<option value="config_splynx.html">Splynx</option>
<option value="config_visp.html">VISP</option>
<option value="config_netzur.html">Netzur</option>
<option value="config_powercode.html">Powercode</option>
<option value="config_sonar.html">Sonar</option>
<option value="config_wispgate.html">WispGate</option>
</select>
<div class="small text-body-secondary mb-3">
Save your provider settings there, then return here if the scheduler still says setup is required.
</div>
<div class="mt-auto d-grid">
<button type="button" class="btn btn-primary" id="btnOpenIntegrationProvider" disabled>
<i class="fa fa-arrow-right"></i> Configure Provider
</button>
<div class="mt-auto d-grid gap-2">
<a class="btn btn-primary" href="config_uisp.html">
<i class="fa fa-arrow-right"></i> Configure UISP
</a>
<a class="btn btn-outline-primary" href="config_splynx.html">Configure Splynx</a>
<a class="btn btn-outline-primary" href="config_visp.html">Configure VISP</a>
<a class="btn btn-outline-primary" href="config_netzur.html">Configure Netzur</a>
<a class="btn btn-outline-primary" href="config_powercode.html">Configure Powercode</a>
<a class="btn btn-outline-primary" href="config_sonar.html">Configure Sonar</a>
<a class="btn btn-outline-primary" href="config_wispgate.html">Configure WispGate</a>
</div>
</div>
</div>
+5 -5
View File
@@ -269,11 +269,11 @@ mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
const VALID_PNG: &[u8] = &[
0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H',
b'D', b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78,
0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, b'I', b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82,
0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H', b'D',
b'R', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, b'I', b'D', b'A', b'T', 0x78, 0x9C, 0x63, 0x00,
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, b'I',
b'E', b'N', b'D', 0xAE, 0x42, 0x60, 0x82,
];
fn temp_runtime_dir() -> std::path::PathBuf {