import { ZabbixConfig, ZabbixHost, ZabbixHostGroup, ZabbixTemplate, ZabbixHostCreateParams, ZabbixHostUpdateParams, ZabbixRpcResponse, } 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; } /** * 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 }; } }