149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
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<T>(method: string, params: Record<string, any>): Promise<T> {
|
|
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<T> = 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<string> {
|
|
const existing = await this.rpc<ZabbixHostGroup[]>('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<ZabbixTemplate | null> {
|
|
const results = await this.rpc<ZabbixTemplate[]>('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<ZabbixHost | null> {
|
|
const results = await this.rpc<ZabbixHost[]>('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<ZabbixHost[]>('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 };
|
|
}
|
|
}
|