mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-10-25 14:20:05 -05:00
changes
This commit is contained in:
1 parent
cc958ccccf
commit
a56549f864
4 files changed
+246
-25
No files matched your search
@@ -27,6 +27,7 @@ Después, ejecute una sincronización manual y valide salidas antes de aumentar
|
||||
- Use la WebUI para confirmar que la importación y la profundidad del árbol son las esperadas.
|
||||
- Use WebUI para ajustes operativos diarios.
|
||||
- Cuando Splynx no puede inferir el padre de un circuito desde `access_device` o los metadatos del router, LibreQoS deja ese circuito sin padre para shaping en lugar de adjuntarlo a un sitio o AP sintético. Corrija el padre en Splynx o Topology Manager si debe aparecer bajo un sitio o AP real.
|
||||
- Si dos filas de hardware de monitoreo de Splynx tienen el mismo título, LibreQoS agrega el ID de hardware de Splynx a esos nombres duplicados de nodos de topología. Por ejemplo, dos AP llamados `Tower South` aparecen como `Tower South (1122)` y `Tower South (1980)`.
|
||||
- En modo `ap_site`, LibreQoS trata los Network Sites de Splynx como contenedores de sitio y las filas de monitoreo con `access_device = 1` como AP o nodos de acceso. Las ramas grandes de sitios y APs, incluidas ramas de uplink o agregación representadas como nodos `Site` o `AP`, pueden virtualizarse automáticamente cuando superan `queue_auto_virtualize_threshold_mbps`, tienen ramas de cola hijas y no tienen circuitos conectados directamente al ID del nodo. Defina una anulación virtual del nodo en `false` cuando un nodo específico deba permanecer como cola física.
|
||||
|
||||
## Validación en 5 minutos después de cambios Splynx
|
||||
|
||||
@@ -26,6 +26,8 @@ LibreQoS supports multiple topology strategies for Splynx integration to balance
|
||||
|
||||
When Splynx cannot infer a circuit parent from `access_device` or router metadata, LibreQoS leaves the circuit unparented for shaping instead of attaching it to a synthetic site or AP. Fix the parent in Splynx or Topology Manager if the circuit should appear under a real site or AP.
|
||||
|
||||
If two Splynx monitoring hardware rows have the same title, LibreQoS appends the Splynx hardware ID to those duplicate topology node names. For example, duplicate APs named `Tower South` appear as `Tower South (1122)` and `Tower South (1980)`.
|
||||
|
||||
In `ap_site` mode, LibreQoS treats Splynx Network Sites as site containers and monitoring rows with `access_device = 1` as AP/access nodes. Large site and AP branches, including uplink or aggregation branches represented as `Site` or `AP` nodes, can be virtualized automatically when they exceed `queue_auto_virtualize_threshold_mbps`, have child queue branches, and have no circuits directly attached to the node ID. Set a node virtual override to `false` when a specific node must remain as a physical queue.
|
||||
|
||||
Configure the shared topology mode in `/etc/lqos.conf` under the `[topology]` section:
|
||||
|
||||
+88
-25
@@ -90,6 +90,47 @@ def normalize_splynx_graph_id(value):
|
||||
return str(value)
|
||||
|
||||
|
||||
def normalize_splynx_display_name(value, fallback):
|
||||
"""
|
||||
Normalize upstream Splynx display names while preserving operator text.
|
||||
"""
|
||||
name = " ".join(str(value or "").split())
|
||||
if name:
|
||||
return name
|
||||
return str(fallback)
|
||||
|
||||
|
||||
def disambiguate_duplicate_hardware_names(hardware_names):
|
||||
"""
|
||||
Append Splynx hardware IDs only when display names collide.
|
||||
"""
|
||||
normalized_names = {
|
||||
hardware_id: normalize_splynx_display_name(display_name, hardware_id)
|
||||
for hardware_id, display_name in hardware_names.items()
|
||||
}
|
||||
names_by_display = {}
|
||||
for hardware_id, display_name in normalized_names.items():
|
||||
names_by_display.setdefault(display_name, []).append(hardware_id)
|
||||
|
||||
unique_names = dict(normalized_names)
|
||||
for display_name, hardware_ids in names_by_display.items():
|
||||
if len(hardware_ids) < 2:
|
||||
continue
|
||||
for hardware_id in hardware_ids:
|
||||
unique_names[hardware_id] = f"{display_name} ({hardware_id})"
|
||||
return unique_names
|
||||
|
||||
|
||||
def bandwidth_for_node_name(siteBandwidth, display_name, fallback_name=None):
|
||||
"""
|
||||
Return bandwidth settings for the displayed node name or its original Splynx name.
|
||||
"""
|
||||
for name in (display_name, fallback_name):
|
||||
if name in siteBandwidth:
|
||||
return siteBandwidth[name]["download"], siteBandwidth[name]["upload"]
|
||||
return 10000, 10000
|
||||
|
||||
|
||||
def supplement_existing_devices_with_online_ips(net, allServices, service_ids_handled, customersOnline, cust_id_to_name, allocated_ipv4s, allocated_ipv6s, device_by_service_id=None):
|
||||
"""
|
||||
For services already handled (devices created from static IPs), supplement missing
|
||||
@@ -270,16 +311,23 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
|
||||
# Build hardware maps for parent selection
|
||||
hardware_name = {}
|
||||
hardware_raw_name = {}
|
||||
hardware_parent = {}
|
||||
hardware_type = {}
|
||||
hardware_name_extended = {}
|
||||
hardware_bandwidth_names = {}
|
||||
ap_nodes = {}
|
||||
if monitoring:
|
||||
for dev in monitoring:
|
||||
dev_id = normalize_splynx_graph_id(dev.get('id'))
|
||||
if dev_id is None:
|
||||
continue
|
||||
dev_name = dev.get('title') or dev.get('name') or dev.get('address') or dev.get('ip') or str(dev_id)
|
||||
raw_dev_name = dev.get('title') or dev.get('name') or dev.get('address') or dev.get('ip')
|
||||
hardware_raw_name[dev_id] = str(raw_dev_name) if raw_dev_name else str(dev_id)
|
||||
dev_name = normalize_splynx_display_name(
|
||||
raw_dev_name,
|
||||
dev_id,
|
||||
)
|
||||
hardware_name[dev_id] = dev_name
|
||||
parent_id = normalize_splynx_graph_id(dev.get('parent_id'))
|
||||
if parent_id is not None:
|
||||
@@ -303,14 +351,26 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
if dev_id is None:
|
||||
continue
|
||||
dev_title = hardware_name.get(dev_id, str(dev_id))
|
||||
raw_dev_title = hardware_raw_name.get(dev_id, str(dev_id))
|
||||
if 'parent_id' in dev and dev_id in hardware_parent and hardware_parent[dev_id] in hardware_name:
|
||||
hardware_name_extended[dev_id] = hardware_name[hardware_parent[dev_id]] + "_" + dev_title
|
||||
hardware_name_extended[dev_id] = normalize_splynx_display_name(
|
||||
hardware_name[hardware_parent[dev_id]] + "_" + dev_title,
|
||||
dev_id,
|
||||
)
|
||||
hardware_bandwidth_names[dev_id] = (
|
||||
hardware_raw_name.get(hardware_parent[dev_id], str(hardware_parent[dev_id]))
|
||||
+ "_"
|
||||
+ raw_dev_title
|
||||
)
|
||||
if dev_id not in hardware_name_extended:
|
||||
hardware_name_extended[dev_id] = dev_title
|
||||
hardware_bandwidth_names[dev_id] = raw_dev_title
|
||||
hardware_name_extended = disambiguate_duplicate_hardware_names(hardware_name_extended)
|
||||
|
||||
# Network sites mappings (new Splynx model)
|
||||
site_id_to_node_id = {}
|
||||
site_id_to_name = {}
|
||||
site_id_to_bandwidth_name = {}
|
||||
site_id_to_address = {}
|
||||
site_id_to_coords = {}
|
||||
if network_sites:
|
||||
@@ -319,12 +379,15 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
if site_id is None:
|
||||
continue
|
||||
node_id = f"ns_{site_id}"
|
||||
name = site.get('title') or site.get('description') or str(site_id)
|
||||
raw_name = site.get('title') or site.get('description')
|
||||
name = normalize_splynx_display_name(raw_name, site_id)
|
||||
address = site.get('address') or name
|
||||
site_id_to_node_id[site_id] = node_id
|
||||
site_id_to_name[site_id] = name
|
||||
site_id_to_bandwidth_name[site_id] = str(raw_name) if raw_name else str(site_id)
|
||||
site_id_to_address[site_id] = address
|
||||
site_id_to_coords[site_id] = parse_gps_pair(site.get('gps'))
|
||||
site_id_to_name = disambiguate_duplicate_hardware_names(site_id_to_name)
|
||||
|
||||
def ap_node_id(raw_id):
|
||||
return f"ap_{raw_id}"
|
||||
@@ -346,11 +409,11 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
for site_id, node_id in site_id_to_node_id.items():
|
||||
nodeName = site_id_to_name.get(site_id, str(site_id))
|
||||
address = site_id_to_address.get(site_id, nodeName)
|
||||
download = 10000
|
||||
upload = 10000
|
||||
if nodeName in siteBandwidth:
|
||||
download = siteBandwidth[nodeName]["download"]
|
||||
upload = siteBandwidth[nodeName]["upload"]
|
||||
download, upload = bandwidth_for_node_name(
|
||||
siteBandwidth,
|
||||
nodeName,
|
||||
site_id_to_bandwidth_name.get(site_id),
|
||||
)
|
||||
node = NetworkNode(id=node_id, displayName=nodeName, type=NodeType.site,
|
||||
parentId=None, download=download, upload=upload, address=address,
|
||||
networkJsonId=f"splynx:network_site:{site_id}",
|
||||
@@ -360,11 +423,11 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
created_ap = 0
|
||||
for ap_id, ap_device in ap_nodes.items():
|
||||
nodeName = hardware_name_extended.get(ap_id, hardware_name.get(ap_id, str(ap_id)))
|
||||
download = 10000
|
||||
upload = 10000
|
||||
if nodeName in siteBandwidth:
|
||||
download = siteBandwidth[nodeName]["download"]
|
||||
upload = siteBandwidth[nodeName]["upload"]
|
||||
download, upload = bandwidth_for_node_name(
|
||||
siteBandwidth,
|
||||
nodeName,
|
||||
hardware_bandwidth_names.get(ap_id),
|
||||
)
|
||||
parent_id = None
|
||||
site_id = ap_device.get('network_site_id')
|
||||
if site_id in site_id_to_node_id:
|
||||
@@ -381,19 +444,19 @@ def run_splynx_pipeline(strategy_name: str):
|
||||
if strategy_name == 'ap_only':
|
||||
print(f"Creating {len(ap_nodes)} AP nodes")
|
||||
for ap_id, ap_device in ap_nodes.items():
|
||||
download = 10000
|
||||
upload = 10000
|
||||
nodeName = hardware_name_extended.get(ap_id, hardware_name.get(ap_id, str(ap_id)))
|
||||
if nodeName in siteBandwidth:
|
||||
download = siteBandwidth[nodeName]["download"]
|
||||
upload = siteBandwidth[nodeName]["upload"]
|
||||
download, upload = bandwidth_for_node_name(
|
||||
siteBandwidth,
|
||||
nodeName,
|
||||
hardware_bandwidth_names.get(ap_id),
|
||||
)
|
||||
latitude, longitude = parse_gps_pair(ap_device.get('gps'))
|
||||
node = NetworkNode(id=ap_id, displayName=nodeName, type=NodeType.ap, parentId=None, download=download, upload=upload, address=None, networkJsonId=f"splynx:ap:{ap_id}", latitude=latitude, longitude=longitude)
|
||||
net.addRawNode(node)
|
||||
return
|
||||
# ap_site and full
|
||||
print("Creating site and AP infrastructure")
|
||||
createInfrastructureNodes(net, monitoring, hardware_name, hardware_parent, hardware_type, siteBandwidth, hardware_name_extended)
|
||||
createInfrastructureNodes(net, monitoring, hardware_name, hardware_parent, hardware_type, siteBandwidth, hardware_name_extended, hardware_bandwidth_names)
|
||||
|
||||
# Parent selector per strategy
|
||||
def select_parent(serviceItem):
|
||||
@@ -733,7 +796,7 @@ def createClientAndDevice(net, serviceItem, cust_id_to_name, downloadForTariffID
|
||||
|
||||
return circuit_id
|
||||
|
||||
def createInfrastructureNodes(net, monitoring, hardware_name, hardware_parent, hardware_type, siteBandwidth, hardware_name_extended):
|
||||
def createInfrastructureNodes(net, monitoring, hardware_name, hardware_parent, hardware_type, siteBandwidth, hardware_name_extended, hardware_bandwidth_names=None):
|
||||
"""
|
||||
Create site and AP nodes from monitoring data.
|
||||
"""
|
||||
@@ -746,12 +809,12 @@ def createInfrastructureNodes(net, monitoring, hardware_name, hardware_parent, h
|
||||
parent_id = None
|
||||
if device_num in hardware_parent.keys():
|
||||
parent_id = normalize_splynx_graph_id(hardware_parent[device_num])
|
||||
download = 10000
|
||||
upload = 10000
|
||||
nodeName = hardware_name_extended[device_num]
|
||||
if nodeName in siteBandwidth:
|
||||
download = siteBandwidth[nodeName]["download"]
|
||||
upload = siteBandwidth[nodeName]["upload"]
|
||||
download, upload = bandwidth_for_node_name(
|
||||
siteBandwidth,
|
||||
nodeName,
|
||||
(hardware_bandwidth_names or {}).get(device_num),
|
||||
)
|
||||
nodeType = hardware_type[device_num]
|
||||
latitude, longitude = monitoring_gps.get(device_num, (None, None))
|
||||
if nodeType == 'AP':
|
||||
|
||||
@@ -181,6 +181,161 @@ class TestIntegrationSplynxStableIds(unittest.TestCase):
|
||||
self.assertEqual(child.parentId, "1")
|
||||
self.assertEqual(child.parentIndex, net.findNodeIndexById("1"))
|
||||
|
||||
def test_duplicate_hardware_names_are_disambiguated_by_id(self):
|
||||
names = integrationSplynx.disambiguate_duplicate_hardware_names({
|
||||
"1122": " Telkom-Braeview-Sec-South ",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
"2000": "Unique AP",
|
||||
})
|
||||
|
||||
self.assertEqual(names["1122"], "Telkom-Braeview-Sec-South (1122)")
|
||||
self.assertEqual(names["1980"], "Telkom-Braeview-Sec-South (1980)")
|
||||
self.assertEqual(names["2000"], "Unique AP")
|
||||
|
||||
def test_display_name_normalization_collapses_internal_whitespace(self):
|
||||
self.assertEqual(
|
||||
integrationSplynx.normalize_splynx_display_name("Tower South\tSector", "1"),
|
||||
"Tower South Sector",
|
||||
)
|
||||
|
||||
def test_disambiguated_name_can_use_original_bandwidth_key(self):
|
||||
download, upload = integrationSplynx.bandwidth_for_node_name(
|
||||
{
|
||||
"Telkom-Braeview-Sec-South": {
|
||||
"download": 500,
|
||||
"upload": 200,
|
||||
}
|
||||
},
|
||||
"Telkom-Braeview-Sec-South (1122)",
|
||||
"Telkom-Braeview-Sec-South",
|
||||
)
|
||||
|
||||
self.assertEqual(download, 500)
|
||||
self.assertEqual(upload, 200)
|
||||
|
||||
def test_bandwidth_lookup_preserves_exact_upstream_name_fallback(self):
|
||||
download, upload = integrationSplynx.bandwidth_for_node_name(
|
||||
{
|
||||
" Sentech Brixton_Sentech - Brixton - Tarana - Sec - South ": {
|
||||
"download": 800,
|
||||
"upload": 300,
|
||||
}
|
||||
},
|
||||
"Sentech Brixton_Sentech - Brixton - Tarana - Sec - South",
|
||||
" Sentech Brixton_Sentech - Brixton - Tarana - Sec - South ",
|
||||
)
|
||||
|
||||
self.assertEqual(download, 800)
|
||||
self.assertEqual(upload, 300)
|
||||
|
||||
def test_duplicate_infrastructure_names_both_export_to_network_json(self):
|
||||
net = integrationCommon.NetworkGraph()
|
||||
hardware_name_extended = integrationSplynx.disambiguate_duplicate_hardware_names({
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
})
|
||||
integrationSplynx.createInfrastructureNodes(
|
||||
net,
|
||||
monitoring=[
|
||||
{"id": "1122", "gps": None},
|
||||
{"id": "1980", "gps": None},
|
||||
],
|
||||
hardware_name={
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
},
|
||||
hardware_parent={},
|
||||
hardware_type={"1122": "AP", "1980": "AP"},
|
||||
siteBandwidth={},
|
||||
hardware_name_extended=hardware_name_extended,
|
||||
hardware_bandwidth_names={
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
},
|
||||
)
|
||||
|
||||
net.prepareTree()
|
||||
network_json = net.buildNetworkJson()
|
||||
|
||||
self.assertIn("Telkom-Braeview-Sec-South (1122)", network_json)
|
||||
self.assertIn("Telkom-Braeview-Sec-South (1980)", network_json)
|
||||
self.assertEqual(
|
||||
network_json["Telkom-Braeview-Sec-South (1122)"]["id"],
|
||||
"splynx:ap:1122",
|
||||
)
|
||||
self.assertEqual(
|
||||
network_json["Telkom-Braeview-Sec-South (1980)"]["id"],
|
||||
"splynx:ap:1980",
|
||||
)
|
||||
|
||||
def test_duplicate_infrastructure_names_preserve_shaped_device_parent_ids(self):
|
||||
net = integrationCommon.NetworkGraph()
|
||||
hardware_name_extended = integrationSplynx.disambiguate_duplicate_hardware_names({
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
})
|
||||
integrationSplynx.createInfrastructureNodes(
|
||||
net,
|
||||
monitoring=[
|
||||
{"id": "1122", "gps": None},
|
||||
{"id": "1980", "gps": None},
|
||||
],
|
||||
hardware_name={
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
},
|
||||
hardware_parent={},
|
||||
hardware_type={"1122": "AP", "1980": "AP"},
|
||||
siteBandwidth={},
|
||||
hardware_name_extended=hardware_name_extended,
|
||||
hardware_bandwidth_names={
|
||||
"1122": "Telkom-Braeview-Sec-South",
|
||||
"1980": "Telkom-Braeview-Sec-South",
|
||||
},
|
||||
)
|
||||
integrationSplynx.createClientAndDevice(
|
||||
net,
|
||||
serviceItem={
|
||||
"id": 1968,
|
||||
"customer_id": 1,
|
||||
"tariff_id": 1,
|
||||
"mac": "",
|
||||
},
|
||||
cust_id_to_name={1: "Octofin"},
|
||||
downloadForTariffID={1: 100},
|
||||
uploadForTariffID={1: 50},
|
||||
parent_node_id="1122",
|
||||
ipv4_list=["192.0.2.1"],
|
||||
ipv6_list=[],
|
||||
)
|
||||
integrationSplynx.createClientAndDevice(
|
||||
net,
|
||||
serviceItem={
|
||||
"id": 35055,
|
||||
"customer_id": 2,
|
||||
"tariff_id": 1,
|
||||
"mac": "",
|
||||
},
|
||||
cust_id_to_name={2: "Second Circuit"},
|
||||
downloadForTariffID={1: 100},
|
||||
uploadForTariffID={1: 50},
|
||||
parent_node_id="1980",
|
||||
ipv4_list=["192.0.2.2"],
|
||||
ipv6_list=[],
|
||||
)
|
||||
|
||||
net.prepareTree()
|
||||
shaped_devices_csv, _circuit_anchors = net.buildShapedDevicesArtifacts()
|
||||
rows = {
|
||||
row["Circuit ID"]: row
|
||||
for row in csv.DictReader(io.StringIO(shaped_devices_csv))
|
||||
}
|
||||
|
||||
self.assertEqual(rows["1968"]["Parent Node"], "Telkom-Braeview-Sec-South (1122)")
|
||||
self.assertEqual(rows["1968"]["Parent Node ID"], "splynx:ap:1122")
|
||||
self.assertEqual(rows["35055"]["Parent Node"], "Telkom-Braeview-Sec-South (1980)")
|
||||
self.assertEqual(rows["35055"]["Parent Node ID"], "splynx:ap:1980")
|
||||
|
||||
def test_find_best_parent_node_normalizes_service_and_sector_ids(self):
|
||||
parent_node_id, assignment_method = integrationSplynx.findBestParentNode(
|
||||
{"router_id": 10, "access_device": 0},
|
||||
|
||||
Reference in new issue
Block a user