mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-10-25 06:17:39 -05:00
changes
This commit is contained in:
1 parent
e11861ea28
commit
fde5c4ebe5
4 files changed
+121
-3
No files matched your search
@@ -46,7 +46,8 @@ flowchart LR
|
||||
- Current builds keep two distinct topology views:
|
||||
- `compatibility_network_json` remains the local compatibility tree used as the base for `network.effective.json`
|
||||
- Insight topology submission derives a separate logical-parent tree from canonical topology state so sites are grouped by logical site hierarchy rather than immediate attachment hops
|
||||
- When possible, the Insight-only logical tree preserves existing `network.json` export names for node identity compatibility instead of renaming nodes to raw canonical labels
|
||||
- For inspection, `lqosd` writes the exact Insight topology JSON it is about to serialize to `/opt/libreqos/src/network.insight.debug.json`
|
||||
- UISP ingest now deduplicates duplicate raw device rows by device ID before graph construction, and graph building defensively skips any residual duplicate device IDs.
|
||||
- After a successful publish, it writes `/opt/libreqos/src/topology_runtime_status.json` with:
|
||||
- `source_generation`
|
||||
- `ready`
|
||||
|
||||
@@ -164,6 +164,16 @@ journalctl -u lqos_scheduler --since "30 minutes ago"
|
||||
|
||||
If Insight shows sites buried under AP/backhaul chains or only a narrow subset of sites loads cleanly, check whether the host is on a build before the separate Insight logical-topology export. Current builds submit an Insight-only logical-parent tree derived from canonical topology state, while `compatibility_network_json` remains reserved for local compatibility and `network.effective.json` generation.
|
||||
|
||||
To inspect the exact topology JSON that `lqosd` is preparing for Insight, review:
|
||||
|
||||
```bash
|
||||
cat /opt/libreqos/src/network.insight.debug.json
|
||||
```
|
||||
|
||||
This debug snapshot is separate from `network.insight.json`. The latter is an installed-runtime input path used by shaping components when Insight topology mode is enabled, while `network.insight.debug.json` is only a diagnostic snapshot of the payload source tree.
|
||||
|
||||
If specific APs or switches appear multiple times with suffixed names such as `... [AP deadbeef]`, check whether UISP is returning duplicate rows for the same device ID. Current builds defensively deduplicate raw UISP devices by `identification.id` before topology graph construction, and skip any residual duplicate device IDs during graph assembly.
|
||||
|
||||
If an integration subprocess fails, current builds keep the scheduler alive, publish a shortened output preview to the scheduler status/error surfaces, and save the full captured output to a timestamped file under `/tmp` such as `lqos_scheduler_uisp_integration_YYYYMMDD_HHMMSS.log`.
|
||||
|
||||
### Scheduler status in WebUI looks unhealthy
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::strategies::full::parse::parse_uisp_datasets;
|
||||
use crate::strategies::full::uisp_fetch::load_uisp_data;
|
||||
use crate::uisp_types::{UispDevice, UispSite, UispSiteType};
|
||||
use lqos_config::Config;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -193,6 +194,55 @@ pub(crate) fn dedup_site_names(sites: &mut [Site]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn dedup_raw_devices_by_id(
|
||||
devices_raw: Vec<Device>,
|
||||
devices_as_json: Vec<Value>,
|
||||
) -> (Vec<Device>, Vec<Value>) {
|
||||
let original_raw_len = devices_raw.len();
|
||||
let original_json_len = devices_as_json.len();
|
||||
if original_raw_len != original_json_len {
|
||||
warn!(
|
||||
raw_devices = original_raw_len,
|
||||
raw_device_json = original_json_len,
|
||||
"UISP device rows and raw device JSON rows differ in length before dedupe"
|
||||
);
|
||||
}
|
||||
|
||||
let mut seen_ids = HashSet::<String>::new();
|
||||
let mut duplicate_counts = HashMap::<String, usize>::new();
|
||||
let mut deduped_devices = Vec::with_capacity(original_raw_len);
|
||||
let mut deduped_json = Vec::with_capacity(original_json_len);
|
||||
|
||||
for (device, raw_json) in devices_raw.into_iter().zip(devices_as_json.into_iter()) {
|
||||
let device_id = device.identification.id.clone();
|
||||
if !seen_ids.insert(device_id.clone()) {
|
||||
*duplicate_counts.entry(device_id).or_insert(0) += 1;
|
||||
continue;
|
||||
}
|
||||
deduped_devices.push(device);
|
||||
deduped_json.push(raw_json);
|
||||
}
|
||||
|
||||
if !duplicate_counts.is_empty() {
|
||||
let duplicate_rows = duplicate_counts.values().sum::<usize>();
|
||||
let mut duplicate_ids = duplicate_counts.into_iter().collect::<Vec<_>>();
|
||||
duplicate_ids.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
||||
let sample_ids = duplicate_ids
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|(device_id, count)| format!("{device_id} (+{count})"))
|
||||
.collect::<Vec<_>>();
|
||||
warn!(
|
||||
duplicate_device_ids = duplicate_ids.len(),
|
||||
duplicate_device_rows = duplicate_rows,
|
||||
sample_ids = ?sample_ids,
|
||||
"UISP returned duplicate device IDs; deduping by device ID before topology processing"
|
||||
);
|
||||
}
|
||||
|
||||
(deduped_devices, deduped_json)
|
||||
}
|
||||
|
||||
impl UispData {
|
||||
pub(crate) fn from_parts(
|
||||
sites_raw: Vec<Site>,
|
||||
@@ -282,6 +332,7 @@ impl UispData {
|
||||
// Obtain the UISP data and transform it into easier to work with types
|
||||
let (mut sites_raw, devices_raw, data_links_raw, devices_as_json) =
|
||||
load_uisp_data(config.clone()).await?;
|
||||
let (devices_raw, devices_as_json) = dedup_raw_devices_by_id(devices_raw, devices_as_json);
|
||||
|
||||
// Normalize endpoint/customer names before deduplication and downstream parsing.
|
||||
normalize_client_site_names(&mut sites_raw);
|
||||
@@ -506,9 +557,9 @@ impl UispData {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{dedup_site_names, normalize_client_site_names};
|
||||
use super::{dedup_raw_devices_by_id, dedup_site_names, normalize_client_site_names};
|
||||
use serde_json::json;
|
||||
use uisp::Site;
|
||||
use uisp::{Device, Site};
|
||||
|
||||
fn mk_site(id: &str, name: &str, address: Option<&str>, service_name: Option<&str>) -> Site {
|
||||
let mut value = json!({
|
||||
@@ -539,6 +590,24 @@ mod tests {
|
||||
serde_json::from_value(value).expect("site JSON must deserialize")
|
||||
}
|
||||
|
||||
fn mk_device(id: &str, name: &str, site_id: &str) -> Device {
|
||||
serde_json::from_value(json!({
|
||||
"identification": {
|
||||
"id": id,
|
||||
"hostname": name,
|
||||
"role": "ap",
|
||||
"site": {
|
||||
"id": site_id,
|
||||
"parent": null
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"status": "active"
|
||||
}
|
||||
}))
|
||||
.expect("device JSON must deserialize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_site_names_prefer_address_segment() {
|
||||
let mut sites = vec![
|
||||
@@ -625,4 +694,34 @@ mod tests {
|
||||
|
||||
assert_eq!(sites[0].name_or_blank(), "Rubio, Jorge");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_raw_devices_are_deduped_by_id_with_matching_json_rows() {
|
||||
let first = mk_device("device-1", "First Name", "site-a");
|
||||
let duplicate = mk_device("device-1", "Second Name", "site-a");
|
||||
let unique = mk_device("device-2", "Unique Name", "site-b");
|
||||
|
||||
let (devices, raw_json) = dedup_raw_devices_by_id(
|
||||
vec![first, duplicate, unique],
|
||||
vec![
|
||||
json!({"identification": {"id": "device-1", "hostname": "First Name"}}),
|
||||
json!({"identification": {"id": "device-1", "hostname": "Second Name"}}),
|
||||
json!({"identification": {"id": "device-2", "hostname": "Unique Name"}}),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(devices.len(), 2);
|
||||
assert_eq!(raw_json.len(), 2);
|
||||
assert_eq!(devices[0].identification.id, "device-1");
|
||||
assert_eq!(devices[0].get_name().as_deref(), Some("First Name"));
|
||||
assert_eq!(devices[1].identification.id, "device-2");
|
||||
assert_eq!(
|
||||
raw_json[0]["identification"]["hostname"].as_str(),
|
||||
Some("First Name")
|
||||
);
|
||||
assert_eq!(
|
||||
raw_json[1]["identification"]["hostname"].as_str(),
|
||||
Some("Unique Name")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -861,6 +861,14 @@ fn add_devices_to_graph(
|
||||
bandwidth_overrides: &[BandwidthOverride],
|
||||
) {
|
||||
for device in uisp_data.devices_raw.iter() {
|
||||
if device_map.contains_key(&device.identification.id) {
|
||||
warn!(
|
||||
device_id = %device.identification.id,
|
||||
device_name = %device.get_name().unwrap_or_default(),
|
||||
"Skipping duplicate UISP device row while building topology graph"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let Some(site_id) = &device.identification.site else {
|
||||
continue;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user