This commit is contained in:
Robert Chacon committed 2026-03-18 19:35:07 -06:00
1 parent d27aaf4071
commit 0b5f0e8354
5 files changed
+133 -35

No files matched your search

+3
View File
@@ -12,6 +12,8 @@ StormGuard es el subsistema de ajuste adaptativo de colas de LibreQoS para event
- Aplica ajustes acotados a límites de nodos configurados para proteger calidad bajo estrés.
- Expone estado y depuración en WebUI.
Los cambios adaptativos de velocidad por sitio de StormGuard se guardan en la capa de overrides de StormGuard. No se escriben de vuelta en `network.json`.
## Configuración
StormGuard se configura en `/etc/lqos.conf` bajo `[stormguard]`.
@@ -69,6 +71,7 @@ También verifique:
- que los nombres en `targets` aún coincidan con `network.json`
- que cambios de integración no hayan renombrado nodos clave
- que los pisos mínimos sean razonables para su perfil de tráfico
- que `network.json` siga reflejando sus velocidades planificadas/de origen si está investigando una reducción inesperada de StormGuard
- que `log_file` (si se usa) sea escribible por el servicio
## Páginas relacionadas
+3
View File
@@ -12,6 +12,8 @@ StormGuard is LibreQoS' adaptive queue-adjustment subsystem for congestion and q
- Applies bounded adjustments to configured node limits to protect quality under stress.
- Exposes status/debug data in the WebUI (Node Manager).
StormGuard adaptive site-speed changes are stored in the StormGuard override layer. They are not written back into `network.json`.
## Configuration
StormGuard is configured in `/etc/lqos.conf` under `[stormguard]`.
@@ -71,6 +73,7 @@ Also verify:
- target node names still match your current `network.json` hierarchy
- integration updates have not renamed key nodes/APs
- your minimum percentage floors are reasonable for expected traffic profiles
- `network.json` still reflects your planned/source-of-truth site rates if you are investigating an unexpected StormGuard reduction
- `log_file` path (if configured) is writable by the service user
## Related Pages
+31 -33
View File
@@ -677,6 +677,10 @@ fn liblqos_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
overrides_network_adjustments_effective,
m
)?)?;
m.add_function(wrap_pyfunction!(
overrides_network_adjustments_materialized,
m
)?)?;
m.add_function(wrap_pyfunction!(is_network_flat, m)?)?;
m.add_function(wrap_pyfunction!(blackboard_finish, m)?)?;
m.add_function(wrap_pyfunction!(blackboard_submit, m)?)?;
@@ -1229,38 +1233,7 @@ fn overrides_network_adjustments(py: Python<'_>) -> PyResult<Vec<PyObject>> {
Err(e) => return Err(PyOSError::new_err(e.to_string())),
};
let mut out: Vec<PyObject> = Vec::new();
for adj in overrides.network_adjustments().iter() {
let d = PyDict::new(py);
match adj {
lqos_overrides::NetworkAdjustment::AdjustSiteSpeed {
site_name,
download_bandwidth_mbps,
upload_bandwidth_mbps,
} => {
d.set_item("type", "adjust_site_speed")?;
d.set_item("site_name", site_name.clone())?;
if let Some(v) = download_bandwidth_mbps {
d.set_item("download_bandwidth_mbps", *v)?;
}
if let Some(v) = upload_bandwidth_mbps {
d.set_item("upload_bandwidth_mbps", *v)?;
}
}
lqos_overrides::NetworkAdjustment::SetNodeVirtual {
node_name,
virtual_node,
} => {
d.set_item("type", "set_node_virtual")?;
d.set_item("node_name", node_name.clone())?;
d.set_item("virtual", *virtual_node)?;
}
}
let obj: PyObject = d.unbind().into();
out.push(obj);
}
Ok(out)
network_adjustments_to_py(py, overrides.network_adjustments())
}
/// Returns the list of network adjustments as Python dicts, using the effective overrides view
@@ -1280,8 +1253,33 @@ fn overrides_network_adjustments_effective(py: Python<'_>) -> PyResult<Vec<PyObj
Err(e) => return Err(PyOSError::new_err(e.to_string())),
};
network_adjustments_to_py(py, overrides.network_adjustments())
}
/// Returns the list of network adjustments that should be materialized into `network.json`.
///
/// This includes operator-owned network adjustments and TreeGuard virtual-node changes, but
/// intentionally excludes StormGuard site-speed adjustments so adaptive runtime rates do not
/// overwrite the operator-authored topology/source-of-truth file.
#[pyfunction]
fn overrides_network_adjustments_materialized(py: Python<'_>) -> PyResult<Vec<PyObject>> {
let config = lqos_config::load_config().map_err(|e| PyOSError::new_err(e.to_string()))?;
let apply_treeguard = config.treeguard.enabled;
let overrides = match lqos_overrides::OverrideStore::load_effective(false, apply_treeguard) {
Ok(o) => o,
Err(e) => return Err(PyOSError::new_err(e.to_string())),
};
network_adjustments_to_py(py, overrides.network_adjustments())
}
fn network_adjustments_to_py(
py: Python<'_>,
adjustments: &[lqos_overrides::NetworkAdjustment],
) -> PyResult<Vec<PyObject>> {
let mut out: Vec<PyObject> = Vec::new();
for adj in overrides.network_adjustments().iter() {
for adj in adjustments.iter() {
let d = PyDict::new(py);
match adj {
lqos_overrides::NetworkAdjustment::AdjustSiteSpeed {
+7 -2
View File
@@ -12,7 +12,8 @@ from liblqos_python import automatic_import_uisp, automatic_import_splynx, queue
automatic_import_powercode, automatic_import_sonar, influx_db_enabled, get_libreqos_directory, \
blackboard_finish, blackboard_submit, automatic_import_wispgate, enable_insight_topology, insight_topology_role, \
automatic_import_netzur, automatic_import_visp, calculate_hash, efficiency_core_ids, scheduler_alive, scheduler_error, \
overrides_persistent_devices_effective, overrides_circuit_adjustments_effective, overrides_network_adjustments_effective, \
overrides_persistent_devices_effective, overrides_circuit_adjustments_effective, \
overrides_network_adjustments_materialized, \
scheduler_output
from apscheduler.schedulers.background import BlockingScheduler
@@ -455,10 +456,14 @@ def apply_network_adjustments(network: dict) -> bool:
Currently supports: adjust_site_speed (by site_name) updating
downloadBandwidthMbps and uploadBandwidthMbps at the matching node, and
set_node_virtual (by node_name) updating the boolean 'virtual' flag.
This path intentionally excludes StormGuard adaptive site-speed overrides so
runtime StormGuard decisions do not overwrite the operator-authored
`network.json` source of truth.
Returns True if any changes were applied.
"""
try:
adjustments = overrides_network_adjustments_effective()
adjustments = overrides_network_adjustments_materialized()
except Exception as e:
print(f"Failed to read network adjustments: {e}")
return False
+89
View File
@@ -34,6 +34,7 @@ def install_scheduler_stubs():
lqlib.overrides_persistent_devices_effective = lambda: []
lqlib.overrides_circuit_adjustments_effective = lambda: []
lqlib.overrides_network_adjustments_effective = lambda: []
lqlib.overrides_network_adjustments_materialized = lambda: []
sys.modules["liblqos_python"] = lqlib
apscheduler_pkg = types.ModuleType("apscheduler")
@@ -282,6 +283,94 @@ class TestSchedulerOverrideMerge(unittest.TestCase):
self.assertEqual(written_rows[0][11], "330")
self.assertEqual(written_rows[0][13], "fq_codel/fq_codel")
def test_apply_network_adjustments_uses_materialized_adjustments(self):
network = {
"Root": {
"downloadBandwidthMbps": 1000,
"uploadBandwidthMbps": 1000,
"children": {
"SiteA": {
"downloadBandwidthMbps": 100,
"uploadBandwidthMbps": 50,
"children": {},
},
"NodeB": {
"downloadBandwidthMbps": 200,
"uploadBandwidthMbps": 100,
"virtual": False,
"children": {},
},
},
}
}
with patch.object(
scheduler,
"overrides_network_adjustments_materialized",
return_value=[
{
"type": "adjust_site_speed",
"site_name": "SiteA",
"download_bandwidth_mbps": 80,
"upload_bandwidth_mbps": 40,
},
{
"type": "set_node_virtual",
"node_name": "NodeB",
"virtual": True,
},
],
):
changed = scheduler.apply_network_adjustments(network)
self.assertTrue(changed)
site = network["Root"]["children"]["SiteA"]
self.assertEqual(site["downloadBandwidthMbps"], 80)
self.assertEqual(site["uploadBandwidthMbps"], 40)
self.assertTrue(network["Root"]["children"]["NodeB"]["virtual"])
def test_apply_network_adjustments_does_not_use_effective_stormguard_speeds(self):
network = {
"Root": {
"downloadBandwidthMbps": 1000,
"uploadBandwidthMbps": 1000,
"children": {
"Pine Hills": {
"downloadBandwidthMbps": 940,
"uploadBandwidthMbps": 500,
"children": {},
}
},
}
}
effective_adjustments = [
{
"type": "adjust_site_speed",
"site_name": "Pine Hills",
"download_bandwidth_mbps": 4,
"upload_bandwidth_mbps": 4,
}
]
with patch.dict(
scheduler.apply_network_adjustments.__globals__,
{"overrides_network_adjustments_effective": lambda: effective_adjustments},
clear=False,
):
with patch.object(
scheduler,
"overrides_network_adjustments_materialized",
return_value=[],
) as mock_materialized:
changed = scheduler.apply_network_adjustments(network)
mock_materialized.assert_called_once_with()
self.assertFalse(changed)
site = network["Root"]["children"]["Pine Hills"]
self.assertEqual(site["downloadBandwidthMbps"], 940)
self.assertEqual(site["uploadBandwidthMbps"], 500)
if __name__ == "__main__":
unittest.main()