diff --git a/.gitignore b/.gitignore index aa244faf..63616819 100644 --- a/.gitignore +++ b/.gitignore @@ -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-* diff --git a/src/LibreQoS.py b/src/LibreQoS.py index faf0564a..607a6b6a 100755 --- a/src/LibreQoS.py +++ b/src/LibreQoS.py @@ -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 = {} diff --git a/src/rust/lqosd/src/node_manager/js_build/src/graphs/cpu_graph.js b/src/rust/lqosd/src/node_manager/js_build/src/graphs/cpu_graph.js index e1b7d306..fc1f115a 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/graphs/cpu_graph.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/graphs/cpu_graph.js @@ -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 index.toString()); + this.option.series.data = usage.map((value) => ({ + value, + itemStyle: {color: lerpGreenToRedViaOrange(100 - value, 100)}, + })); this.chart.setOption(this.option); } } - diff --git a/src/rust/lqosd/src/node_manager/local_api/circuit.rs b/src/rust/lqosd/src/node_manager/local_api/circuit.rs index 0e191a87..25f944ca 100644 --- a/src/rust/lqosd/src/node_manager/local_api/circuit.rs +++ b/src/rust/lqosd/src/node_manager/local_api/circuit.rs @@ -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 { .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() { diff --git a/src/rust/lqosd/src/shaped_devices_tracker/mod.rs b/src/rust/lqosd/src/shaped_devices_tracker/mod.rs index ae17b5e8..223650fb 100644 --- a/src/rust/lqosd/src/shaped_devices_tracker/mod.rs +++ b/src/rust/lqosd/src/shaped_devices_tracker/mod.rs @@ -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 { + 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(); } diff --git a/src/test_virtual_tree_nodes.py b/src/test_virtual_tree_nodes.py index 61726bb9..fb73a815 100644 --- a/src/test_virtual_tree_nodes.py +++ b/src/test_virtual_tree_nodes.py @@ -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() diff --git a/src/virtual_tree_nodes.py b/src/virtual_tree_nodes.py index c784414a..a3d5f853 100644 --- a/src/virtual_tree_nodes.py +++ b/src/virtual_tree_nodes.py @@ -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