/** * Shared utilities for Zabbix WAN host management. * Used by both the RMM discovery sync and the manual host creation endpoints. */ import { ZabbixClient } from '@/lib/services/zabbix-client'; import { ZabbixHostMacro, ZabbixHostTag, ZabbixHostCreateParams } from '@/lib/types/zabbix'; // --------------------------------------------------------------------------- // ISP info type // --------------------------------------------------------------------------- export interface IspInfo { isp: string; // "Comcast Cable Communications, LLC" asn: string; // "AS7922" city: string; region: string; country: string; } // --------------------------------------------------------------------------- // Zabbix host technical name sanitization // Zabbix rejects: + ' , . & ( ) and other special chars in the `host` field. // We sanitize to alphanumeric, spaces, hyphens, underscores only. // The display `name` field is left as-is (accepts any UTF-8). // --------------------------------------------------------------------------- export function sanitizeHostname(name: string): string { return name .replace(/[^a-zA-Z0-9 \-_]/g, '') // strip disallowed chars .replace(/\s+/g, ' ') // collapse multiple spaces .trim(); } // --------------------------------------------------------------------------- // ISP lookup via ipinfo.io (free, no key required for basic fields) // Results are cached within the module lifetime to avoid duplicate lookups. // Call clearIspCache() at the start of each request if needed. // --------------------------------------------------------------------------- const ispCache = new Map(); export function clearIspCache(): void { ispCache.clear(); } export async function lookupIsp(ip: string): Promise { if (ispCache.has(ip)) return ispCache.get(ip)!; try { const token = process.env.IPINFO_TOKEN; const headers: Record = { Accept: 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`https://ipinfo.io/${ip}/json`, { headers, cache: 'no-store', signal: AbortSignal.timeout(6000), }); if (!res.ok) { ispCache.set(ip, null); return null; } const data = await res.json(); // org field format: "AS7922 Comcast Cable Communications, LLC" const org: string = data.org ?? ''; const m = org.match(/^(AS\d+)\s+(.+)$/); const info: IspInfo = { isp: m ? m[2] : org, asn: m ? m[1] : '', city: data.city ?? '', region: data.region ?? '', country: data.country ?? '', }; ispCache.set(ip, info); return info; } catch { ispCache.set(ip, null); return null; } } // --------------------------------------------------------------------------- // Build the full Zabbix host upsert params from common inputs. // --------------------------------------------------------------------------- export interface BuildHostParamsInput { siteName: string; wanIp: string; companyId?: number; companyName?: string; rmmSiteUid?: string; ispInfo: IspInfo | null; multiWan?: boolean; allIps?: string[]; singleDeviceFallback?: boolean; onlineDeviceCount?: number; source?: string; // tag value for "source" (default "datto-rmm") icmpTemplateId: string | null; globalGroupId: string; zabbix: ZabbixClient; } export async function buildHostParams(input: BuildHostParamsInput): Promise { const { siteName, wanIp, companyId, companyName, rmmSiteUid, ispInfo, multiWan, allIps, singleDeviceFallback, onlineDeviceCount, source, icmpTemplateId, globalGroupId, zabbix, } = input; const templates = icmpTemplateId ? [{ templateid: icmpTemplateId }] : undefined; // Build groups: always global, + per-client, + per-ISP const groups: Array<{ groupid: string }> = [{ groupid: globalGroupId }]; if (companyName) { const clientGroupId = await zabbix.ensureHostGroup(`Clients/${companyName}`); groups.push({ groupid: clientGroupId }); } if (ispInfo?.isp) { const ispGroupId = await zabbix.ensureHostGroup(`ISP/${ispInfo.isp}`); groups.push({ groupid: ispGroupId }); } // Build macros: Autotask identity + ISP context const macros: ZabbixHostMacro[] = []; if (companyId && companyName) { macros.push( { macro: '{$AUTOTASK_COMPANY_ID}', value: String(companyId), description: 'Autotask company ID' }, { macro: '{$AUTOTASK_COMPANY_NAME}', value: companyName, description: 'Autotask company name' }, ); } if (rmmSiteUid) { macros.push( { macro: '{$RMM_SITE_UID}', value: rmmSiteUid, description: 'Datto RMM site UID' }, ); } if (ispInfo) { macros.push( { macro: '{$ISP_NAME}', value: ispInfo.isp, description: 'ISP / carrier name' }, { macro: '{$ASN}', value: ispInfo.asn, description: 'Autonomous System Number' }, { macro: '{$ISP_CITY}', value: ispInfo.city, description: 'City (from IP geolocation)' }, { macro: '{$ISP_REGION}', value: ispInfo.region, description: 'Region (from IP geolocation)' }, { macro: '{$ISP_COUNTRY}', value: ispInfo.country, description: 'Country code (from IP geolocation)' }, ); } if (multiWan && allIps) { macros.push({ macro: '{$MULTI_WAN_IPS}', value: allIps.join(', '), description: 'All public IPs seen (multi-WAN site)' }); } // Build tags: for dashboard filtering and problem correlation const sourceTag = source ?? 'datto-rmm'; const tags: ZabbixHostTag[] = [{ tag: 'source', value: sourceTag }]; if (companyName) { tags.push({ tag: 'client', value: companyName }); } if (ispInfo?.isp) { tags.push({ tag: 'isp', value: ispInfo.isp }); } if (ispInfo?.asn) { tags.push({ tag: 'asn', value: ispInfo.asn }); } if (multiWan) { tags.push({ tag: 'multi-wan', value: 'true' }); } if (singleDeviceFallback) { tags.push({ tag: 'single-device-fallback', value: 'true' }); } const descParts = [ onlineDeviceCount != null ? `Datto RMM site – WAN IP from ${onlineDeviceCount} online devices` : `Manual host – WAN IP ${wanIp}`, ispInfo ? `ISP: ${ispInfo.isp} (${ispInfo.asn}) — ${ispInfo.city}, ${ispInfo.region}, ${ispInfo.country}` : null, multiWan && allIps ? `Multi-WAN detected: ${allIps.join(', ')}` : null, singleDeviceFallback ? `Note: IP sourced from single device (no multi-device confirmation)` : null, ].filter(Boolean).join('\n'); return { host: sanitizeHostname(siteName), name: siteName, description: descParts, interfaces: [{ type: 1, main: 1, useip: 1, ip: wanIp, dns: '', port: '10050' }], groups, templates, macros: macros.length > 0 ? macros : undefined, tags, }; } // --------------------------------------------------------------------------- // Discover ICMP template — tries multiple common names // --------------------------------------------------------------------------- const ICMP_TEMPLATE_NAMES = [ 'ICMP Ping', 'Template Module ICMP Ping', 'Template Module ICMP Ping by Zabbix agent', ]; export async function discoverIcmpTemplate(zabbix: ZabbixClient): Promise { for (const name of ICMP_TEMPLATE_NAMES) { const tmpl = await zabbix.findTemplate(name); if (tmpl) return tmpl.templateid; } return null; }