feat: virtual tree nodes + physical CPU site tree

Implements logical-only (virtual) nodes in network.json to improve CPU spreading while preserving monitoring hierarchy (NLNet milestones a/b/c).

Virtual nodes

- network.json supports "virtual": true (and legacy type=virtual) as logical-only nodes.

- Physical HTB topology skips virtual nodes and promotes their children to the nearest real ancestor.

- Writes logical->physical mapping + virtual node list into queuingStructure.json for debugging/inspection.

CPU placement + binpacking

- When binpacking is enabled but Insight/planner is unavailable, fall back to deterministic round-robin CPU assignment (still honors virtual promotion).

- Adds sanity checks/fallbacks to avoid degenerate single-bin assignments collapsing the physical tree.

UI + diagnostics

- Config/network UI supports toggling virtual state and shows a ghost icon for virtual nodes.

- Adds cpu_tree.html (ECharts) and websocket endpoint to visualize the PHYSICAL per-CPU site tree from queuingStructure.json.

Overrides

- lqos_overrides: add SetNodeVirtual adjustment plus CLI commands to view/edit/delete by node name.

- Scheduler applies virtual overrides onto network.json so periodic rebuilds don’t clobber them.

Config loader hardening

- lqos_config errors now reference /etc/lqos.conf (or LQOS_CONFIG) instead of deprecated ispConfig.py.

- Avoids caching initial config-load failures; can recover if config appears later.

- Treats versionless v1.5 configs as 1.5 by injecting version key; avoids accidental 1.4 migration.

- Removes a panic() in legacy TOML parsing path.

Misc

- Fix LibreQoS.py exit calls (os.exit -> os._exit) and add Python unittest coverage for virtual node promotion.
This commit is contained in:
Herbert Wolverson committed 2026-02-04 14:29:24 -06:00
1 parent 1943ae77f4
commit 69cfd6d77b
25 files changed
+1060 -113

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import unittest
from virtual_tree_nodes import (
build_logical_to_physical_node_map,
build_physical_network,
is_virtual_node,
)
class TestVirtualTreeNodes(unittest.TestCase):
def test_is_virtual_node(self):
self.assertTrue(is_virtual_node({"virtual": True}))
self.assertTrue(is_virtual_node({"type": "virtual"}))
self.assertTrue(is_virtual_node({"type": "VIRTUAL"}))
self.assertFalse(is_virtual_node({"virtual": False}))
self.assertFalse(is_virtual_node({"type": "Site"}))
self.assertFalse(is_virtual_node({}))
def test_logical_to_physical_mapping_and_physical_promotion(self):
logical = {
"Region": {
"downloadBandwidthMbps": 1000,
"uploadBandwidthMbps": 1000,
"children": {
"Town": {
"downloadBandwidthMbps": 500,
"uploadBandwidthMbps": 500,
"virtual": True,
"children": {
"AP_A": {
"downloadBandwidthMbps": 200,
"uploadBandwidthMbps": 200,
}
},
},
"AP_B": {
"downloadBandwidthMbps": 300,
"uploadBandwidthMbps": 300,
},
},
}
}
mapping, virtual_nodes = build_logical_to_physical_node_map(logical)
self.assertIn("Town", virtual_nodes)
self.assertEqual(mapping["Town"], "Region")
self.assertEqual(mapping["Region"], "Region")
self.assertEqual(mapping["AP_A"], "AP_A")
physical = build_physical_network(logical)
self.assertIn("Region", physical)
self.assertNotIn("Town", physical["Region"].get("children", {}))
self.assertIn("AP_A", physical["Region"].get("children", {}))
self.assertIn("AP_B", physical["Region"].get("children", {}))
self.assertNotIn("virtual", physical["Region"])
def test_promotion_collision_raises(self):
logical = {
"Region": {
"downloadBandwidthMbps": 1000,
"uploadBandwidthMbps": 1000,
"children": {
"AP_A": {"downloadBandwidthMbps": 300, "uploadBandwidthMbps": 300},
"Town": {
"downloadBandwidthMbps": 500,
"uploadBandwidthMbps": 500,
"virtual": True,
"children": {
"AP_A": {"downloadBandwidthMbps": 200, "uploadBandwidthMbps": 200}
},
},
},
}
}
with self.assertRaises(ValueError):
build_physical_network(logical)
if __name__ == "__main__":
unittest.main()