From 86f758d79a29110a7e9948e1dc723ca71327f820 Mon Sep 17 00:00:00 2001 From: rchac Date: Tue, 21 Apr 2026 22:50:26 +0000 Subject: [PATCH] changes --- docs-es/v2.0/integrations-sonar-es.md | 1 + docs/v2.0/integrations-sonar.md | 1 + src/integrationSonar.py | 29 ++++++++++++++++++++++++++- src/test_integration_sonar.py | 15 +++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs-es/v2.0/integrations-sonar-es.md b/docs-es/v2.0/integrations-sonar-es.md index 17c9baa3..85fdaf27 100644 --- a/docs-es/v2.0/integrations-sonar-es.md +++ b/docs-es/v2.0/integrations-sonar-es.md @@ -15,6 +15,7 @@ Use esta integración cuando Sonar sea su fuente de verdad CRM/NMS. - La integración refresca automáticamente los datos importados y de shaping que LibreQoS usa con Sonar. - `network.json` queda para despliegues DIY o manuales. - La integración importa IPs desde equipos de inventario, cuentas Radius e IPs asignadas directamente a la cuenta Sonar. Las IPs de Radius y de cuenta se agregan como dispositivos suplementarios de shaping y se deduplican contra las IPs de inventario. +- Las asignaciones IPv4 e IPv6 se separan en los campos correctos de LibreQoS. Los prefijos IPv6 de Sonar, como redes delegadas `/56`, se importan como asignaciones IPv6. - Use la WebUI para confirmar que la importación terminó correctamente. ## Referencia completa diff --git a/docs/v2.0/integrations-sonar.md b/docs/v2.0/integrations-sonar.md index a032aff8..24766596 100644 --- a/docs/v2.0/integrations-sonar.md +++ b/docs/v2.0/integrations-sonar.md @@ -8,6 +8,7 @@ Current behavior notes: - Paginated Sonar GraphQL requests now use a split connect/read timeout and retry transient read timeouts before failing the import. - Emitted Sonar identities are namespaced (for example `sonar:account:` and `sonar:device:`) so they remain stable across overrides and downstream tooling. - Account device discovery now preserves inventory-item IP handling and also imports Radius account and account-level IP assignments when they exist. Inventory-backed MACs are still used for AP mapping; Radius-only and account-level IPs are added as supplemental shaping devices and overlapping subnets are de-duplicated. +- IPv4 and IPv6 assignments are split into the correct LibreQoS fields. Sonar IPv6 prefixes such as delegated `/56` networks are imported as IPv6 assignments, not IPv4 device entries. - Sonar `child_accounts` are also imported when they expose their own service and usable IP data. If a child account lacks its own address, LibreQoS falls back to the parent account address so the child can still be emitted as its own circuit. - Sonar settings now support ISP-specific recurring-service fallback rates plus a recurring-service exclusion list. LibreQoS still prefers active `DATA` services first; recurring mappings are only used when an account has no usable `DATA` service. - If Sonar returns non-JSON content or GraphQL errors, the integration now raises a more specific error message showing the endpoint and a short response preview. diff --git a/src/integrationSonar.py b/src/integrationSonar.py index b87990af..4fba6110 100644 --- a/src/integrationSonar.py +++ b/src/integrationSonar.py @@ -1,5 +1,6 @@ from pythonCheck import checkPythonVersion checkPythonVersion() +import ipaddress import requests import subprocess import time @@ -227,6 +228,23 @@ def dedupeSubnets(subnets): return deduped +def splitIpVersions(subnets): + ipv4 = [] + ipv6 = [] + invalid = [] + for subnet in subnets or []: + try: + network = ipaddress.ip_network(subnet, strict=False) + except ValueError: + invalid.append(subnet) + continue + if network.version == 4: + ipv4.append(subnet) + else: + ipv6.append(subnet) + return ipv4, ipv6, invalid + + def normalizeServiceName(name): return (name or "").strip().casefold() @@ -773,6 +791,8 @@ def buildMacToApMap(aps): def sonarImportSummary(sites, aps, accounts): total_devices = 0 devices_with_ips = 0 + ipv4_assignments = 0 + ipv6_assignments = 0 account_level_ip_devices = 0 accounts_with_parent = 0 for account in accounts: @@ -782,12 +802,16 @@ def sonarImportSummary(sites, aps, accounts): total_devices += 1 if len(device.get('ips') or []) > 0: devices_with_ips += 1 + ipv4, ipv6, _invalid = splitIpVersions(device.get('ips', [])) + ipv4_assignments += len(ipv4) + ipv6_assignments += len(ipv6) if device.get('source') == 'account_ip_assignments': account_level_ip_devices += 1 return ( f"Sonar import summary: sites={len(sites)} aps={len(aps)} " f"accounts={len(accounts)} accounts_with_ap_parent={accounts_with_parent} " f"devices={total_devices} devices_with_ips={devices_with_ips} " + f"ipv4_assignments={ipv4_assignments} ipv6_assignments={ipv6_assignments} " f"account_level_ip_devices={account_level_ip_devices}" ) @@ -836,7 +860,10 @@ def createShaper(): net.addRawNode(customer) for device in account['devices']: - libre_device = NetworkNode(id=device['id'], displayName=device['name'],parentId=account['id'],type=NodeType.device, ipv4=device['ips'],ipv6=[],mac=device['mac']) + ipv4, ipv6, invalid_ips = splitIpVersions(device.get('ips', [])) + if invalid_ips: + print(f"Skipping invalid Sonar IP assignment(s) for {device['id']}: {invalid_ips}") + libre_device = NetworkNode(id=device['id'], displayName=device['name'],parentId=account['id'],type=NodeType.device, ipv4=ipv4,ipv6=ipv6,mac=device['mac']) net.addRawNode(libre_device) net.prepareTree() diff --git a/src/test_integration_sonar.py b/src/test_integration_sonar.py index 6d29a77f..aff493a8 100644 --- a/src/test_integration_sonar.py +++ b/src/test_integration_sonar.py @@ -180,6 +180,17 @@ class TestIntegrationSonarDevices(unittest.TestCase): self.assertEqual(devices[1]["mac"], "") self.assertEqual(devices[1]["source"], "account_ip_assignments") + def test_split_ip_versions_preserves_ipv6_prefixes(self): + ipv4, ipv6, invalid = integrationSonar.splitIpVersions([ + "100.64.8.80", + "2605:40c1:300::/56", + "not an ip", + ]) + + self.assertEqual(ipv4, ["100.64.8.80"]) + self.assertEqual(ipv6, ["2605:40c1:300::/56"]) + self.assertEqual(invalid, ["not an ip"]) + def test_account_level_ips_are_deduped_against_inventory_and_radius_ips(self): devices = integrationSonar.buildAccountDevices( sonar_account( @@ -429,7 +440,7 @@ class TestIntegrationSonarDevices(unittest.TestCase): }, { "id": "sonar:account-ip-assignments:1", - "ips": ["100.64.11.11"], + "ips": ["100.64.11.11", "2605:40c1:300::/56"], "source": "account_ip_assignments", }, ], @@ -447,6 +458,8 @@ class TestIntegrationSonarDevices(unittest.TestCase): self.assertIn("accounts_with_ap_parent=1", summary) self.assertIn("devices=2", summary) self.assertIn("devices_with_ips=1", summary) + self.assertIn("ipv4_assignments=1", summary) + self.assertIn("ipv6_assignments=1", summary) self.assertIn("account_level_ip_devices=1", summary)