- EquipmentDetail: collapsible node groups (clients grouped by mesh node) - Signal strength as RSSI % (0-255 per 802.11-2020) with 10-tone color scale - Management IP clickable link to device web GUI (/superadmin/) - Fibre status compact top bar (status + Rx/Tx power when available) - targo-hub: WAN IP detection across all VLAN interfaces - targo-hub: full WiFi client count (direct + EasyMesh mesh repeaters) - targo-hub: /devices/:id/hosts endpoint with client-to-node mapping - ClientsPage: start empty, load only on search (no auto-load all) - nginx: dynamic ollama resolver (won't crash if ollama is down) - Cleanup: remove unused BillingKPIs.vue and TagInput.vue - New docs and migration scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
"""
|
|
migrate_provisioning_data.py — Migrate WiFi and VoIP provisioning data
|
|
from GenieACS MariaDB into ERPNext Service Equipment custom fields.
|
|
|
|
Sources:
|
|
- provisioning-data.json (exported from GenieACS MariaDB at 10.100.80.100)
|
|
Contains: wifi[] (1,713 entries) and voip[] (797 entries)
|
|
|
|
Matching strategy:
|
|
- WiFi: wifi.serial (Deco MAC) → Service Equipment.mac_address
|
|
- VoIP: voip.serial (RCMG serial) → Service Equipment.serial_number
|
|
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/migrate_provisioning_data.py
|
|
"""
|
|
import frappe
|
|
import json
|
|
import os
|
|
|
|
os.chdir("/home/frappe/frappe-bench/sites")
|
|
frappe.init(site="erp.gigafibre.ca", sites_path=".")
|
|
frappe.connect()
|
|
frappe.local.flags.ignore_permissions = True
|
|
print("Connected:", frappe.local.site)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Load provisioning data export
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# This file should be copied to the bench directory from:
|
|
# scripts/migration/genieacs-export/provisioning-data.json
|
|
PROV_FILE = "/home/frappe/frappe-bench/provisioning-data.json"
|
|
if not os.path.exists(PROV_FILE):
|
|
# Try the git repo path
|
|
PROV_FILE = os.path.expanduser(
|
|
"~/frappe-bench/apps/gigafibre-fsm/scripts/migration/genieacs-export/provisioning-data.json"
|
|
)
|
|
if not os.path.exists(PROV_FILE):
|
|
print("ERROR: provisioning-data.json not found. Copy it to /home/frappe/frappe-bench/")
|
|
print("Source: scripts/migration/genieacs-export/provisioning-data.json")
|
|
exit(1)
|
|
|
|
with open(PROV_FILE) as f:
|
|
prov_data = json.load(f)
|
|
|
|
wifi_entries = prov_data.get("wifi", [])
|
|
voip_entries = prov_data.get("voip", [])
|
|
print("WiFi entries: {}".format(len(wifi_entries)))
|
|
print("VoIP entries: {}".format(len(voip_entries)))
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Build MAC → Equipment Name map
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\nBuilding MAC address and serial number maps...")
|
|
|
|
# MAC address → Service Equipment name (normalized to uppercase, no colons)
|
|
mac_map = {}
|
|
rows = frappe.db.sql("""
|
|
SELECT name, mac_address FROM "tabService Equipment"
|
|
WHERE mac_address IS NOT NULL AND mac_address != ''
|
|
""", as_dict=True)
|
|
for r in rows:
|
|
mac_clean = r["mac_address"].upper().replace(":", "").replace("-", "").replace(".", "")
|
|
mac_map[mac_clean] = r["name"]
|
|
print(" MAC map: {} entries".format(len(mac_map)))
|
|
|
|
# Serial number → Service Equipment name
|
|
serial_map = {}
|
|
rows = frappe.db.sql("""
|
|
SELECT name, serial_number FROM "tabService Equipment"
|
|
WHERE serial_number IS NOT NULL AND serial_number != ''
|
|
""", as_dict=True)
|
|
for r in rows:
|
|
serial_map[r["serial_number"].upper()] = r["name"]
|
|
print(" Serial map: {} entries".format(len(serial_map)))
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 1. WiFi provisioning → Service Equipment
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("1. WiFi PROVISIONING")
|
|
print("=" * 60)
|
|
|
|
# Group wifi entries by serial (keep latest / instance=1)
|
|
wifi_by_serial = {}
|
|
for w in wifi_entries:
|
|
serial = w.get("serial", "")
|
|
if not serial:
|
|
continue
|
|
instance = w.get("instance", 1)
|
|
# Prefer instance 1 (primary SSID)
|
|
if serial not in wifi_by_serial or instance == 1:
|
|
wifi_by_serial[serial] = w
|
|
print("Unique WiFi serials: {}".format(len(wifi_by_serial)))
|
|
|
|
matched_wifi = 0
|
|
unmatched_wifi = 0
|
|
|
|
for serial, w in wifi_by_serial.items():
|
|
ssid = w.get("ssid", "")
|
|
password = w.get("password", "")
|
|
if not ssid:
|
|
continue
|
|
|
|
# WiFi serial is typically a MAC address (Deco) — try MAC map
|
|
mac_clean = serial.upper().replace(":", "").replace("-", "").replace(".", "")
|
|
eq_name = mac_map.get(mac_clean)
|
|
|
|
# Also try serial number map
|
|
if not eq_name:
|
|
eq_name = serial_map.get(serial.upper())
|
|
|
|
# Try with common prefix patterns (TPLG...)
|
|
if not eq_name:
|
|
for prefix in ["TPLG", "TPLINK"]:
|
|
eq_name = serial_map.get(prefix + serial.upper())
|
|
if eq_name:
|
|
break
|
|
|
|
if not eq_name:
|
|
unmatched_wifi += 1
|
|
continue
|
|
|
|
frappe.db.sql("""
|
|
UPDATE "tabService Equipment"
|
|
SET wifi_ssid = %(ssid)s, wifi_password = %(password)s
|
|
WHERE name = %(name)s
|
|
""", {"name": eq_name, "ssid": ssid, "password": password})
|
|
matched_wifi += 1
|
|
|
|
frappe.db.commit()
|
|
print("WiFi matched: {} / unmatched: {}".format(matched_wifi, unmatched_wifi))
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 2. VoIP provisioning → Service Equipment
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("2. VoIP PROVISIONING")
|
|
print("=" * 60)
|
|
|
|
# Group voip entries by serial
|
|
voip_by_serial = {}
|
|
for v in voip_entries:
|
|
serial = v.get("serial", "")
|
|
if not serial:
|
|
continue
|
|
instance = v.get("instance", 1)
|
|
if serial not in voip_by_serial or instance == 1:
|
|
voip_by_serial[serial] = v
|
|
print("Unique VoIP serials: {}".format(len(voip_by_serial)))
|
|
|
|
matched_voip = 0
|
|
unmatched_voip = 0
|
|
|
|
for serial, v in voip_by_serial.items():
|
|
username = v.get("username", "")
|
|
password = v.get("password", "")
|
|
if not username:
|
|
continue
|
|
|
|
# VoIP serial is typically RCMG physical serial — try serial map
|
|
eq_name = serial_map.get(serial.upper())
|
|
|
|
# Also try with RCMG prefix
|
|
if not eq_name and not serial.upper().startswith("RCMG"):
|
|
eq_name = serial_map.get("RCMG" + serial.upper())
|
|
|
|
# Try MAC map (some CWMP serials encode MAC)
|
|
if not eq_name:
|
|
# Extract last 12 chars as potential MAC
|
|
if len(serial) >= 12:
|
|
potential_mac = serial[-12:].upper()
|
|
eq_name = mac_map.get(potential_mac)
|
|
|
|
if not eq_name:
|
|
unmatched_voip += 1
|
|
continue
|
|
|
|
frappe.db.sql("""
|
|
UPDATE "tabService Equipment"
|
|
SET sip_username = %(username)s, sip_password = %(password)s
|
|
WHERE name = %(name)s
|
|
""", {"name": eq_name, "username": username, "password": password})
|
|
matched_voip += 1
|
|
|
|
frappe.db.commit()
|
|
print("VoIP matched: {} / unmatched: {}".format(matched_voip, unmatched_voip))
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 3. DONE
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "=" * 60)
|
|
print("PROVISIONING MIGRATION COMPLETE")
|
|
print("=" * 60)
|
|
print("WiFi: {} matched, {} unmatched".format(matched_wifi, unmatched_wifi))
|
|
print("VoIP: {} matched, {} unmatched".format(matched_voip, unmatched_voip))
|
|
print("")
|
|
print("Unmatched devices will be resolved after OLT query tagging (see ANALYSIS.md)")
|