Merge pull request #1036 from LibreQoE/uisp_fix_3

changes
This commit is contained in:
Robert Chacón authored and GitHub committed 2026-04-07 21:20:22 -06:00
commit 7fc42a4039
7 files changed
+112 -20

No files matched your search

+1
View File
@@ -178,6 +178,7 @@ src/network.large.json
/src/topology_effective_state.json
/src/topology_overrides.json
/src/topology_parent_candidates.json
/src/topology_canonical_state.json
# Migrated integration compatibility artifacts
/src/integrationUISPbandwidths.csv.migrated-*
+18
View File
@@ -24,6 +24,7 @@ from deepdiff import DeepDiff
from virtual_tree_nodes import (
build_logical_to_physical_node_map,
build_physical_network,
collect_physical_parent_node_aliases,
is_virtual_node,
)
from shaping_skip_report import (
@@ -1210,6 +1211,23 @@ def refreshShapers():
return newDict
network = flattenA(network, 1)
# After flattening, some attachment-style names may only survive as
# metadata on the physical node that now owns that branch.
parent_node_aliases = collect_physical_parent_node_aliases(network)
for circuit in subscriberCircuits:
parent_node = str(circuit.get('ParentNode', '') or '').strip()
if not parent_node or parent_node == 'none':
continue
resolved_parent = parent_node_aliases.get(parent_node)
if resolved_parent and resolved_parent != parent_node:
logging.info(
"Resolved circuit parent alias '%s' to physical queue node '%s' for circuit '%s'",
parent_node,
resolved_parent,
circuit.get('circuitID', ''),
)
circuit['ParentNode'] = resolved_parent
# Group circuits by parent node. Reduces runtime for section below this one.
circuits_by_parent_node = {}
circuit_min_down_combined_by_parent_node = {}
@@ -4,19 +4,10 @@ import {lerpGreenToRedViaOrange} from "../helpers/scaling";
export class CpuHistogram extends DashboardGraph {
constructor(id) {
super(id);
let d = [];
let axis = [];
for (let i=0; i<20; i++) {
d.push({
value: i,
itemStyle: {color: lerpGreenToRedViaOrange(20-i, 20)},
});
axis.push(i.toString());
}
this.option = {
xAxis: {
type: 'category',
data: axis,
data: [],
},
yAxis: {
type: 'value',
@@ -24,7 +15,7 @@ export class CpuHistogram extends DashboardGraph {
max: 100,
},
series: {
data: d,
data: [],
type: 'bar',
}
};
@@ -33,14 +24,12 @@ export class CpuHistogram extends DashboardGraph {
update(cpu) {
this.chart.hideLoading();
this.option.series.data = [];
for (let i=0; i<cpu.length; i++) {
this.option.series.data.push({
value: cpu[i],
itemStyle: {color: lerpGreenToRedViaOrange(100-cpu[i], 100)},
});
}
const usage = Array.isArray(cpu) ? cpu : [];
this.option.xAxis.data = usage.map((_, index) => index.toString());
this.option.series.data = usage.map((value) => ({
value,
itemStyle: {color: lerpGreenToRedViaOrange(100 - value, 100)},
}));
this.chart.setOption(this.option);
}
}
@@ -1,4 +1,5 @@
use crate::node_manager::local_api::ethernet_caps::ethernet_advisory_for_circuit;
use crate::shaped_devices_tracker::resolve_parent_node_alias;
use crate::shaped_devices_tracker::SHAPED_DEVICES;
use lqos_config::{CircuitEthernetMetadata, ShapedDevice};
use serde::{Deserialize, Serialize};
@@ -31,6 +32,12 @@ pub fn circuit_by_id_data(id: &str) -> Option<CircuitByIdData> {
.iter()
.filter(|d| d.circuit_id.to_lowercase().trim() == safe_id)
.cloned()
.map(|mut device| {
if let Some(resolved_parent) = resolve_parent_node_alias(&device.parent_node) {
device.parent_node = resolved_parent;
}
device
})
.collect();
if devices.is_empty() {
@@ -491,6 +491,27 @@ pub fn map_node_names(nodes: &[usize]) -> BusResponse {
BusResponse::NodeNames(result)
}
pub fn resolve_parent_node_alias(parent_node: &str) -> Option<String> {
let trimmed = parent_node.trim();
if trimmed.is_empty() {
return None;
}
let reader = NETWORK_JSON.read();
let nodes = reader.get_nodes_when_ready();
if nodes.iter().any(|node| node.name == trimmed) {
return Some(trimmed.to_string());
}
nodes.iter().find_map(|node| {
node.active_attachment_name
.as_deref()
.filter(|alias| alias.trim() == trimmed)
.map(|_| node.name.clone())
})
}
pub fn get_funnel(circuit_id: &str) -> BusResponse {
let reader = NETWORK_JSON.read();
if let Some(index) = reader.get_index_for_name(circuit_id) {
@@ -553,7 +574,10 @@ pub fn get_all_circuits() -> BusResponse {
circuit_name = Some(device.circuit_name.clone());
device_id = Some(device.device_id.clone());
device_name = Some(device.device_name.clone());
parent_node = Some(device.parent_node.clone());
parent_node = Some(
resolve_parent_node_alias(&device.parent_node)
.unwrap_or_else(|| device.parent_node.clone()),
);
plan.down = device.download_max_mbps.round();
plan.up = device.upload_max_mbps.round();
}
+19
View File
@@ -3,6 +3,7 @@ import unittest
from virtual_tree_nodes import (
build_logical_to_physical_node_map,
build_physical_network,
collect_physical_parent_node_aliases,
is_virtual_node,
)
@@ -75,6 +76,24 @@ class TestVirtualTreeNodes(unittest.TestCase):
with self.assertRaises(ValueError):
build_physical_network(logical)
def test_collect_physical_parent_node_aliases_includes_active_attachment_name(self):
physical = {
"Hoodoo Hill": {
"children": {
"Thunderhill Core": {
"name": "Thunderhill Core",
"active_attachment_name": "Thunderhill-Hoodoo",
"children": {},
}
}
}
}
aliases = collect_physical_parent_node_aliases(physical)
self.assertEqual(aliases["Thunderhill Core"], "Thunderhill Core")
self.assertEqual(aliases["Thunderhill-Hoodoo"], "Thunderhill Core")
if __name__ == "__main__":
unittest.main()
+34
View File
@@ -91,3 +91,37 @@ def build_physical_network(logical_network):
return physical
def collect_physical_parent_node_aliases(network):
"""
Returns a mapping of acceptable parent-node aliases to the physical node key
that exists in the shaping tree.
This lets shaping-time code resolve attachment-style names that may survive
in `ShapedDevices.csv` even after the physical tree is flattened or promoted.
"""
aliases = {}
def recurse(level):
if not isinstance(level, dict):
return
for key, node in level.items():
if not isinstance(key, str) or not isinstance(node, dict):
continue
candidates = [key, node.get("name"), node.get("active_attachment_name")]
for candidate in candidates:
if candidate is None:
continue
alias = str(candidate).strip()
if not alias:
continue
aliases.setdefault(alias, key)
children = node.get("children")
if isinstance(children, dict):
recurse(children)
recurse(network)
return aliases