import { ZabbixConfig, ZabbixHost, ZabbixHostGroup, ZabbixTemplate, ZabbixHostCreateParams, ZabbixHostUpdateParams, ZabbixRpcResponse, ZabbixProblem, ZabbixEvent, } from '@/lib/types/zabbix'; export type { ZabbixHostTag } from '@/lib/types/zabbix'; export class ZabbixClient { private config: ZabbixConfig; private rpcId = 0; constructor(config: ZabbixConfig) { this.config = config; } /** * Make a JSON-RPC 2.0 call to the Zabbix API. * Auth is stateless: Bearer token in Authorization header (Zabbix 6.0+ API token). */ private async rpc(method: string, params: Record): Promise { const id = ++this.rpcId; const url = `${this.config.apiUrl.replace(/\/$/, '')}/api_jsonrpc.php`; const body = JSON.stringify({ jsonrpc: '2.0', method, params, id, }); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiToken}`, }, body, }); if (!response.ok) { const text = await response.text(); throw new Error(`Zabbix HTTP ${response.status}: ${text.substring(0, 200)}`); } const data: ZabbixRpcResponse = await response.json(); if (data.error) { throw new Error( `Zabbix RPC error (${data.error.code}): ${data.error.message}` + (data.error.data ? ` — ${data.error.data}` : '') ); } return data.result as T; } /** * Get or create a host group by name. Returns the groupid. */ async ensureHostGroup(name: string): Promise { const existing = await this.rpc('hostgroup.get', { output: ['groupid', 'name'], filter: { name: [name] }, }); if (existing.length > 0) { return existing[0].groupid; } const created = await this.rpc<{ groupids: string[] }>('hostgroup.create', { name, }); return created.groupids[0]; } /** * Find a template by exact or partial host name. Returns the first match or null. */ async findTemplate(name: string): Promise { const results = await this.rpc('template.get', { output: ['templateid', 'host', 'name'], search: { host: name }, searchByAny: false, }); return results.length > 0 ? results[0] : null; } /** * Find a host by its technical name (host field), with a fallback search by * display name. The fallback handles legacy hosts created before hostname * sanitization was introduced (their host == name == full display string). */ async findHostByName(hostname: string, displayName?: string): Promise { const results = await this.rpc('host.get', { output: ['hostid', 'host', 'name', 'status'], filter: { host: [hostname] }, }); if (results.length > 0) return results[0]; // Fallback: search by display name (catches pre-sanitization legacy hosts) if (displayName && displayName !== hostname) { const byName = await this.rpc('host.get', { output: ['hostid', 'host', 'name', 'status'], filter: { name: [displayName] }, }); if (byName.length > 0) return byName[0]; } return null; } /** * Fetch all hosts with full detail: interfaces, tags, macros, groups. */ async getHosts(): Promise { return this.rpc('host.get', { output: 'extend', selectInterfaces: 'extend', selectTags: 'extend', selectMacros: 'extend', selectGroups: 'extend', selectParentTemplates: ['templateid', 'host', 'name'], }); } /** * Delete one or more hosts by their hostids. */ async deleteHosts(hostids: string[]): Promise { await this.rpc<{ hostids: string[] }>('host.delete', hostids); } /** * Get open problems with hosts inline. * Only returns problems whose trigger is linked to at least one enabled host. * severities: 2=Warning,3=Average,4=High,5=Disaster */ async getOpenProblems(minSeverity = 2): Promise { return this.rpc('problem.get', { output: 'extend', selectAcknowledges: 'extend', selectSuppressionData: 'extend', severities: [2, 3, 4, 5].filter(s => s >= minSeverity), recent: true, sortfield: 'eventid', sortorder: 'DESC', }); } /** * Get problems that were resolved within the given window. * Uses problem.get with time_from/time_till (filters on problem clock) and * r_eventid IS SET (meaning the problem has a recovery event = resolved). * Returns problems only — r_clock is the resolution timestamp. */ async getResolvedEvents(from: Date, to: Date): Promise { const fromSec = Math.floor(from.getTime() / 1000); const toSec = Math.floor(to.getTime() / 1000); // event.get with value:1 returns PROBLEM trigger events. // time_from/time_till filter on event creation (problem start) time. // We then keep only those that have an r_clock (= resolved) within the window. const events = await this.rpc('event.get', { output: 'extend', source: 0, object: 0, value: 1, time_from: fromSec, time_till: toSec, severities: [2, 3, 4, 5], sortfield: 'eventid', sortorder: 'DESC', limit: 500, }); return events.filter(e => e.r_eventid && e.r_eventid !== '0'); } /** * Fetch group names for a specific list of hostids. * Returns a Map for client name resolution. */ async getHostGroupMap(hostids: string[]): Promise> { if (hostids.length === 0) return new Map(); const hosts = await this.rpc; }>>('host.get', { output: ['hostid'], hostids, selectGroups: ['groupid', 'name'], }); const map = new Map(); for (const h of hosts) { map.set(h.hostid, (h.groups ?? []).map((g: { name: string }) => g.name)); } return map; } /** * For a list of triggerids, return a map of triggerid → { hostid, hostName } * filtered to only enabled hosts (status '0'). * Triggerids with no enabled host are omitted from the map. */ async getTriggerEnabledHosts(triggerids: string[]): Promise> { if (triggerids.length === 0) return new Map(); const triggers = await this.rpc; }>>('trigger.get', { output: ['triggerid'], triggerids, selectHosts: ['hostid', 'name', 'status'], }); const map = new Map(); for (const t of triggers) { const enabled = (t.hosts ?? []).find(h => h.status === '0'); if (enabled) map.set(t.triggerid, { hostid: enabled.hostid, hostName: enabled.name }); } return map; } /** * Create or update a Zabbix host. Idempotent — looks up by host name first. * Returns the hostid and whether the host was created or updated. */ async upsertHost(params: ZabbixHostCreateParams): Promise<{ action: 'created' | 'updated'; hostid: string; }> { const existing = await this.findHostByName(params.host, params.name); if (!existing) { const result = await this.rpc<{ hostids: string[] }>('host.create', params); return { action: 'created', hostid: result.hostids[0] }; } const updateParams: ZabbixHostUpdateParams = { hostid: existing.hostid, name: params.name, description: params.description, // Do not pass interfaces on update — Zabbix rejects changes when items // are already linked to the existing interface. groups: params.groups, templates: params.templates, macros: params.macros, tags: params.tags, }; await this.rpc<{ hostids: string[] }>('host.update', updateParams); return { action: 'updated', hostid: existing.hostid }; } }