/** * POST /api/itglue/sync-wan * Pulls all Internet/WAN flexible assets (type 3794) from the local itg_flexible_assets * table, parses static IPs, and upserts into itg_wan_circuits. * Also matches Zabbix WAN hosts by IP to populate zabbix_hostid. */ import { NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; export const maxDuration = 120; // Extract public IPs from the raw HTML/text blob IT Glue stores function parseStaticIps(raw: string | null): string[] { if (!raw) return []; // Strip HTML tags const text = raw.replace(/<[^>]+>/g, ' ').replace(/ /g, ' '); // Match IPv4 addresses const matches = text.match(/\b(\d{1,3}\.){3}\d{1,3}\b/g) ?? []; // Filter out private/RFC1918 ranges, subnet masks, gateways that look like masks return [...new Set( matches.filter(ip => { const parts = ip.split('.').map(Number); if (parts[0] === 10) return false; if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false; if (parts[0] === 192 && parts[1] === 168) return false; if (parts[0] === 255) return false; if (parts[0] === 0) return false; if (ip === 'DHCP') return false; return true; }) )]; } function isDecommissioned(notes: string | null): boolean { if (!notes) return false; const lower = notes.toLowerCase(); return lower.includes('decommission') || lower.includes('decomission') || lower.includes('retired') || lower.includes('removed'); } export async function POST() { try { // Pull all IT Glue WAN flexible assets joined to org data const assets = await postgresClient.query<{ id: number; organization_id: number; org_name: string; psa_id: string | null; provider: string | null; link_type: string | null; static_ips_raw: string | null; upload_mbps: string | null; download_mbps: string | null; location_name: string | null; location_city: string | null; notes: string | null; }>(` SELECT fa.id, fa.organization_id, io.name AS org_name, io.psa_id, fa.traits->>'provider' AS provider, fa.traits->>'link-type' AS link_type, fa.traits->>'static-ip-address-es' AS static_ips_raw, fa.traits->>'upload-speed-mbps' AS upload_mbps, fa.traits->>'download-speed-mbps' AS download_mbps, fa.traits->'location-s'->'values'->0->>'name' AS location_name, fa.traits->'location-s'->'values'->0->>'city' AS location_city, fa.traits->>'notes' AS notes FROM itg_flexible_assets fa JOIN itg_organizations io ON io.id = fa.organization_id WHERE fa.flexible_asset_type_id = 3794 `); // Build IP → hostid map from zabbix_wan_hosts for matching const zabbixRows = await postgresClient.query<{ hostid: string; wan_ip: string }>( 'SELECT hostid, wan_ip FROM zabbix_wan_hosts WHERE wan_ip IS NOT NULL' ); const zabbixByIp = new Map(); for (const r of zabbixRows.rows) { zabbixByIp.set(r.wan_ip, r.hostid); } let upserted = 0; for (const a of assets.rows) { const staticIps = parseStaticIps(a.static_ips_raw); const decommissioned = isDecommissioned(a.notes); const autotaskCompanyId = a.psa_id ? Number(a.psa_id) : null; // Try to match a Zabbix host by any of the circuit's static IPs let zabbixHostid: string | null = null; for (const ip of staticIps) { if (zabbixByIp.has(ip)) { zabbixHostid = zabbixByIp.get(ip)!; break; } } await postgresClient.query( `INSERT INTO itg_wan_circuits ( id, organization_id, org_name, autotask_company_id, provider, link_type, static_ips, raw_ip_text, upload_mbps, download_mbps, location_name, location_city, notes, is_decommissioned, zabbix_hostid, last_synced_at, updated_at ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW(),NOW()) ON CONFLICT (id) DO UPDATE SET org_name = EXCLUDED.org_name, autotask_company_id = EXCLUDED.autotask_company_id, provider = EXCLUDED.provider, link_type = EXCLUDED.link_type, static_ips = EXCLUDED.static_ips, raw_ip_text = EXCLUDED.raw_ip_text, upload_mbps = EXCLUDED.upload_mbps, download_mbps = EXCLUDED.download_mbps, location_name = EXCLUDED.location_name, location_city = EXCLUDED.location_city, notes = EXCLUDED.notes, is_decommissioned = EXCLUDED.is_decommissioned, zabbix_hostid = EXCLUDED.zabbix_hostid, last_synced_at = NOW(), updated_at = NOW()`, [ a.id, a.organization_id, a.org_name, autotaskCompanyId, a.provider, a.link_type, staticIps, a.static_ips_raw, a.upload_mbps ? Number(a.upload_mbps) : null, a.download_mbps ? Number(a.download_mbps) : null, a.location_name, a.location_city, a.notes, decommissioned, zabbixHostid, ] ); upserted++; } // Summary stats const stats = await postgresClient.query<{ total: string; with_ip: string; matched_zabbix: string; decommissioned: string; no_itg_org_link: string; }>(` SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE array_length(static_ips,1) > 0) AS with_ip, COUNT(*) FILTER (WHERE zabbix_hostid IS NOT NULL) AS matched_zabbix, COUNT(*) FILTER (WHERE is_decommissioned) AS decommissioned, COUNT(*) FILTER (WHERE autotask_company_id IS NULL) AS no_itg_org_link FROM itg_wan_circuits `); return NextResponse.json({ upserted, ...stats.rows[0] }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return NextResponse.json({ error: msg }, { status: 500 }); } }