""" 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 legacy-db) 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)")