249 lines
9.4 KiB
Python
249 lines
9.4 KiB
Python
"""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()
|