202 lines
6.8 KiB
Python
202 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Match RADIUS PPPoE usernames to Gaiia accounts by exact name-to-username mapping.
|
|
Deterministic only — no fuzzy matching. Leaves ambiguous/unmatched rows blank.
|
|
|
|
A Gaiia displayName like "Jose Ortiz" generates candidate usernames:
|
|
joseortiz, jortiz, joseo, jose, ortiz
|
|
The RADIUS username must match one of these exactly.
|
|
"""
|
|
|
|
import csv
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
GAIIA_KEY = "gaiia_sk_live_955b27e3680747a69ad40b403a912155_b0d1be3c"
|
|
GAIIA_ENDPOINT = "https://api.gaiia.com/api/v1"
|
|
|
|
|
|
def fetch_all_accounts():
|
|
"""Fetch all Gaiia accounts with id and displayName."""
|
|
accounts = []
|
|
cursor = None
|
|
while True:
|
|
after_arg = f', after: "{cursor}"' if cursor else ""
|
|
query = f"""
|
|
query {{
|
|
accounts(first: 250{after_arg}) {{
|
|
nodes {{ id readableId name displayName }}
|
|
pageInfo {{ hasNextPage endCursor }}
|
|
}}
|
|
}}
|
|
"""
|
|
result = subprocess.run(
|
|
[
|
|
"curl", "-s",
|
|
"-X", "POST", GAIIA_ENDPOINT,
|
|
"-H", "Content-Type: application/json",
|
|
"-H", f"X-Gaiia-Api-Key: {GAIIA_KEY}",
|
|
"-H", "x-timezone: America/Chicago",
|
|
"-d", json.dumps({"query": query}),
|
|
],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
data = json.loads(result.stdout)
|
|
nodes = data["data"]["accounts"]["nodes"]
|
|
for n in nodes:
|
|
name = n.get("displayName") or n.get("name") or ""
|
|
if name:
|
|
accounts.append((n["id"], name.strip(), n["readableId"]))
|
|
page_info = data["data"]["accounts"]["pageInfo"]
|
|
if not page_info["hasNextPage"]:
|
|
break
|
|
cursor = page_info["endCursor"]
|
|
return accounts
|
|
|
|
|
|
def generate_username_variants(display_name):
|
|
"""Generate candidate PPPoE-style usernames from a Gaiia display name.
|
|
|
|
"Jose Ortiz" -> {joseortiz, jortiz, jose, ortiz}
|
|
"Graham McIntire" -> {grahammcintire, gmcintire, graham, mcintire}
|
|
"Kory Biggs Shop" -> {korybiggsshop, kbiggs, kory, biggs, ...}
|
|
"Mid Valley Pumping LLC" -> {midvalleypumpingllc, midvalley, ...}
|
|
"""
|
|
name = display_name.lower().strip()
|
|
# Remove punctuation
|
|
name = re.sub(r'[^\w\s]', '', name)
|
|
name = re.sub(r'\s+', ' ', name).strip()
|
|
parts = name.split()
|
|
|
|
variants = set()
|
|
|
|
# Full concatenation: firstlast with no spaces
|
|
variants.add("".join(parts))
|
|
|
|
# First initial + last name: e.g. jortiz, gmcintire
|
|
if len(parts) >= 2:
|
|
variants.add(parts[0][0] + parts[-1])
|
|
# Also first name + last name initial
|
|
variants.add(parts[0] + parts[-1][0])
|
|
|
|
# Each individual part
|
|
for p in parts:
|
|
variants.add(p)
|
|
|
|
# First + last (just the two parts concatenated)
|
|
if len(parts) >= 2:
|
|
variants.add(parts[0] + parts[-1])
|
|
|
|
# Handle numbered suffixes (e.g. "Perla Chavez 2" -> "perlachavez2")
|
|
if len(parts) >= 3 and re.match(r'^\d+$', parts[-1]):
|
|
variants.add("".join(parts[:-1]) + parts[-1])
|
|
|
|
return variants
|
|
|
|
|
|
def parse_radius_input(text):
|
|
"""Parse the pipe-delimited RADIUS user table."""
|
|
users = []
|
|
pattern = r'\|\s*(\d+)\s*\|\s*(\S+)\s*\|\s*Clear(?:t|T)ext-Password\s*\|\s*:=\s*\|\s*(\S+)\s*\|'
|
|
for match in re.finditer(pattern, text):
|
|
users.append({
|
|
"radius_id": int(match.group(1)),
|
|
"username": match.group(2),
|
|
"password": match.group(3),
|
|
})
|
|
return users
|
|
|
|
|
|
def match_users(radius_users, accounts):
|
|
"""Match RADIUS users to Gaiia accounts by exact username variant matching."""
|
|
# Build mapping: username_variant -> list of (account_id, account_name, readable_id)
|
|
variant_map = defaultdict(list)
|
|
for acct_id, acct_name, readable_id in accounts:
|
|
for variant in generate_username_variants(acct_name):
|
|
variant_map[variant].append((acct_id, acct_name, readable_id))
|
|
|
|
# Track which Gaiia accounts have been used (to avoid double-matching)
|
|
used_accounts = set()
|
|
|
|
results = []
|
|
for user in radius_users:
|
|
username = user["username"].lower()
|
|
candidates = variant_map.get(username, [])
|
|
|
|
# Filter out already-used accounts
|
|
available = [(a, n, r) for a, n, r in candidates if a not in used_accounts]
|
|
|
|
if len(available) == 1:
|
|
acct_id, acct_name, readable_id = available[0]
|
|
used_accounts.add(acct_id)
|
|
results.append({
|
|
**user,
|
|
"gaiia_id": acct_id,
|
|
"gaiia_name": acct_name,
|
|
"gaiia_readable_id": readable_id,
|
|
"match_type": "exact",
|
|
})
|
|
elif len(available) > 1:
|
|
# Multiple candidates — leave blank for manual review
|
|
results.append({
|
|
**user,
|
|
"gaiia_id": "",
|
|
"gaiia_name": f"AMBIGUOUS: {', '.join(n for _,n,_ in available)}",
|
|
"gaiia_readable_id": "",
|
|
"match_type": "ambiguous",
|
|
})
|
|
else:
|
|
results.append({
|
|
**user,
|
|
"gaiia_id": "",
|
|
"gaiia_name": "",
|
|
"gaiia_readable_id": "",
|
|
"match_type": "no_match",
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
def main():
|
|
text = sys.stdin.read()
|
|
|
|
radius_users = parse_radius_input(text)
|
|
print(f"Parsed {len(radius_users)} RADIUS users", file=sys.stderr)
|
|
|
|
print("Fetching Gaiia accounts...", file=sys.stderr)
|
|
accounts = fetch_all_accounts()
|
|
print(f"Fetched {len(accounts)} Gaiia accounts with names", file=sys.stderr)
|
|
|
|
results = match_users(radius_users, accounts)
|
|
|
|
writer = csv.writer(sys.stdout)
|
|
writer.writerow(["radius_id", "username", "password", "gaiia_id", "gaiia_name", "gaiia_readable_id", "match_type"])
|
|
for r in results:
|
|
writer.writerow([
|
|
r["radius_id"], r["username"], r["password"],
|
|
r["gaiia_id"], r["gaiia_name"], r["gaiia_readable_id"], r["match_type"],
|
|
])
|
|
|
|
matched = sum(1 for r in results if r["gaiia_id"])
|
|
ambiguous = sum(1 for r in results if r["match_type"] == "ambiguous")
|
|
unmatched = sum(1 for r in results if r["match_type"] == "no_match")
|
|
print(f"\nMatched: {matched}, Ambiguous: {ambiguous}, No match: {unmatched} (total: {len(results)})", file=sys.stderr)
|
|
|
|
if ambiguous:
|
|
print("\nAMBIGUOUS (multiple Gaiia accounts match same username):", file=sys.stderr)
|
|
for r in results:
|
|
if r["match_type"] == "ambiguous":
|
|
print(f" {r['username']}: {r['gaiia_name']}", file=sys.stderr)
|
|
|
|
if unmatched:
|
|
print("\nNO MATCH:", file=sys.stderr)
|
|
for r in results:
|
|
if r["match_type"] == "no_match":
|
|
print(f" {r['username']}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|