"""Compare PPPoE active sessions from climax MikroTik against Gaiia accounts. Checks: 1. Is the PPPoE username found as a Gaiia account? 2. Does the Gaiia account's inventory item IP match the PPPoE IP? Usage: cd /Users/graham/dev/network/gaiia && uv run python compare_pppoe.py """ from __future__ import annotations import os import sys from typing import Any sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) from gaiia.client import GaiiaClient # PPPoE active sessions from climax (2026-07-02) PPPOE_SESSIONS: list[dict[str, str]] = [ {"name": "charlesboone", "address": "100.64.7.247", "mac": "C8:9E:43:E3:99:EA"}, {"name": "amberprater", "address": "100.64.7.243", "mac": "6C:CD:D6:2C:31:94"}, {"name": "matthewgoodwin", "address": "100.64.7.241", "mac": "34:98:B5:44:42:A4"}, {"name": "robbymccollom", "address": "100.64.7.239", "mac": "D8:B3:70:92:0C:C6"}, {"name": "gordonhamilton", "address": "100.64.7.237", "mac": "A0:63:91:3A:29:B1"}, {"name": "chasewilliams", "address": "100.64.7.235", "mac": "38:94:ED:5B:82:E9"}, {"name": "cynthiajones", "address": "100.64.7.233", "mac": "E4:FA:C4:56:CA:D9"}, {"name": "janicealexander", "address": "100.64.7.225", "mac": "E0:D3:62:E8:65:C5"}, {"name": "michaelray", "address": "204.110.188.38", "mac": "9C:05:D6:59:FF:63"}, {"name": "marysmelser", "address": "100.64.7.218", "mac": "00:01:9F:33:54:B9"}, {"name": "eddieyarbrough", "address": "100.64.7.216", "mac": "10:C3:7B:42:D8:B8"}, {"name": "timgilbert", "address": "100.64.7.213", "mac": "94:A6:7E:EE:98:B8"}, {"name": "chadwhitsell", "address": "100.64.7.211", "mac": "04:95:E6:76:3C:91"}, {"name": "donnacampbell", "address": "100.64.7.209", "mac": "74:FE:CE:3A:6D:AC"}, {"name": "bryangoulart", "address": "100.64.7.205", "mac": "8C:90:2D:E9:90:7A"}, {"name": "crystalharney", "address": "100.64.7.201", "mac": "98:25:4A:04:B6:43"}, {"name": "johnvayo", "address": "100.64.7.197", "mac": "98:25:4A:04:B5:89"}, {"name": "richardbarragan", "address": "100.64.7.193", "mac": "8C:90:2D:E9:98:E1"}, {"name": "carolstrickland", "address": "100.64.7.189", "mac": "E8:DA:00:18:54:45"}, {"name": "timbagert", "address": "100.64.7.140", "mac": "74:FE:CE:3A:6D:8E"}, {"name": "douggarber", "address": "100.64.7.136", "mac": "00:01:9F:33:79:A9"}, {"name": "janetkern", "address": "100.64.7.61", "mac": "28:C6:8E:C1:06:AF"}, {"name": "gregmcintire", "address": "100.64.7.59", "mac": "66:62:8B:10:10:65"}, {"name": "jenniferboon", "address": "100.64.7.35", "mac": "98:25:4A:04:BA:EA"}, {"name": "glendabeauchamp", "address": "100.64.7.15", "mac": "8C:90:2D:E9:97:85"}, {"name": "jmichaelculverhouse", "address": "100.64.7.250", "mac": "34:98:B5:31:D3:3A"}, {"name": "elviraquezada", "address": "100.64.7.249", "mac": "E0:C2:50:47:DC:1A"}, {"name": "tammieventris", "address": "100.64.7.232", "mac": "98:25:4A:04:A5:E1"}, {"name": "taekim", "address": "204.110.188.42", "mac": "9C:5C:8E:44:85:B4"}, {"name": "ruthfengler", "address": "100.64.7.246", "mac": "A0:63:91:25:F6:E5"}, {"name": "buddyswan", "address": "100.64.7.245", "mac": "00:01:9F:34:D2:21"}, {"name": "rhondabolton", "address": "100.64.7.231", "mac": "A0:04:60:94:D5:FF"}, ] # Known PPPoE-name → Gaiia-displayName mismatches MANUAL_MAP: dict[str, str] = { "bryangoulart": "Brian Goulart", # typo in PPPoE username } def flatten(name: str) -> str: """Lowercase and remove spaces/punctuation from a name.""" return name.lower().replace(" ", "").replace("-", "").replace("'", "") def fetch_all_accounts(client: GaiiaClient) -> list[dict[str, Any]]: """Fetch all Gaiia accounts (id, readableId, displayName).""" query = """ query($after: String) { accounts(first: 50, after: $after) { nodes { id readableId displayName } pageInfo { hasNextPage endCursor } totalCount } } """ accounts: list[dict[str, Any]] = [] cursor: str | None = None has_next = True while has_next: variables: dict[str, Any] = {} if cursor: variables["after"] = cursor data = client.query(query, variables) conn = data["accounts"] accounts.extend(conn.get("nodes") or []) has_next = conn["pageInfo"]["hasNextPage"] cursor = conn["pageInfo"].get("endCursor") print(f" Fetched {len(accounts)}/{conn.get('totalCount', '?')} accounts...", file=sys.stderr) return accounts def fetch_account_inventory( client: GaiiaClient, account_id: str ) -> list[dict[str, Any]]: """Fetch inventory items for a specific account, including IP addresses.""" query = """ query($id: GlobalID!) { account(id: $id) { assignedInventoryItems(first: 10) { nodes { inventoryItem { id ipAddressV4 ipAddressV6 model { name manufacturer { name } } } } } } } """ data = client.query(query, {"id": account_id}) acct = data.get("account") if not acct: return [] items = acct.get("assignedInventoryItems", {}) assignations = items.get("nodes") or [] # Extract the actual inventory items from assignations result = [] for assign in assignations: item = assign.get("inventoryItem") if item: result.append(item) return result def main() -> None: api_key = os.environ.get("GAIIA_KEY") if not api_key: print("Error: GAIIA_KEY env var not set", file=sys.stderr) sys.exit(1) client = GaiiaClient(api_key, timezone="America/Chicago") print("Fetching all accounts from Gaiia...", file=sys.stderr) accounts = fetch_all_accounts(client) print(f"Total: {len(accounts)} accounts\n", file=sys.stderr) # Build lookup: flattened displayName → account by_flat: dict[str, dict[str, Any]] = {} for a in accounts: by_flat[flatten(a.get("displayName", ""))] = a # --- Step 1: Match PPPoE names to Gaiia accounts --- print("=" * 75) print("CLIMAX PPPoE → GAIIA ACCOUNT MATCHING") print("=" * 75) matched: list[dict[str, Any]] = [] unmatched: list[dict[str, Any]] = [] for session in PPPOE_SESSIONS: pname = session["name"] flat = flatten(pname) account = by_flat.get(flat) # Try manual map if not account and pname in MANUAL_MAP: mapped_flat = flatten(MANUAL_MAP[pname]) account = by_flat.get(mapped_flat) session["_account"] = account if account: matched.append(session) else: unmatched.append(session) print(f"{'PPPoE':<25} {'PPPoE IP':<18} {'Gaiia Name':<25} {'Account ID':<8}") print("-" * 78) for s in matched: acc = s["_account"] print(f"{s['name']:<25} {s['address']:<18} {acc['displayName']:<25} {acc['readableId']:<8}") for s in unmatched: print(f"{s['name']:<25} {s['address']:<18} {'(not in Gaiia)':<25} {'-':<8}") print(f"\nMatched: {len(matched)} | Not in Gaiia: {len(unmatched)}") if unmatched: print("Not in Gaiia:") for s in unmatched: print(f" - {s['name']} ({s['address']})") # --- Step 2: Compare IP addresses from inventory items --- print(f"\n{'=' * 75}") print("IP ADDRESS COMPARISON (PPPoE active IP vs Gaiia inventory item IP)") print("=" * 75) ip_matched = 0 ip_mismatched = 0 ip_no_inventory = 0 ip_not_checked = 0 for s in matched: acc = s["_account"] pppoe_ip = s["address"] pppoe_name = s["name"] # Fetch inventory items for this account items = fetch_account_inventory(client, acc["id"]) s["_items"] = items if not items: print(f"{pppoe_name:<25} {pppoe_ip:<18} (no inventory items in Gaiia)") ip_no_inventory += 1 continue # Check if any inventory item has an IP matching the PPPoE IP found_match = False for item in items: item_ip = item.get("ipAddressV4") if item_ip == pppoe_ip: model = item.get("model", {}) model_name = model.get("name", "?") if model else "?" print(f"{pppoe_name:<25} {pppoe_ip:<18} MATCH (item: {model_name})") ip_matched += 1 found_match = True break if not found_match: item_ips = [i.get("ipAddressV4") for i in items if i.get("ipAddressV4")] if item_ips: print(f"{pppoe_name:<25} {pppoe_ip:<18} MISMATCH (Gaiia has: {', '.join(item_ips)})") ip_mismatched += 1 else: print(f"{pppoe_name:<25} {pppoe_ip:<18} NO IP SET (has inventory items but no IPs)") ip_no_inventory += 1 print(f"\n--- Summary ---") print(f"Accounts in Gaiia: {len(matched)}/{len(PPPOE_SESSIONS)}") print(f"Not in Gaiia: {len(unmatched)}") print(f"IP matches PPPoE: {ip_matched}") print(f"IP mismatch (different IP): {ip_mismatched}") print(f"No inventory / no IP in Gaiia: {ip_no_inventory}") if unmatched: print(f"\nAccounts missing from Gaiia (need investigation):") for s in unmatched: mac = s.get("mac", "") print(f" {s['name']} IP={s['address']} MAC={mac}") client.close() if __name__ == "__main__": main()