feat: add template and template group management via GraphQL

- Implemented GraphQL endpoints for importing, querying, and deleting Zabbix templates and template groups. - Added support for full template data import, including items, preprocessing steps, tags, and linked templates. - Implemented dependent item support by deferred creation logic in the template importer. - Added ability to query templates and template groups with name pattern filtering (supporting Zabbix wildcards). - Implemented batch deletion for templates and template groups by ID or name pattern. - Improved error reporting by including detailed Zabbix API error data in GraphQL responses. - Added comprehensive unit and integration tests covering all new functionality. - Provided GraphQL sample queries and mutations in the 'docs' directory for all new endpoints.
This commit is contained in:
Andreas Hilbig 2026-01-24 15:42:13 +01:00
parent e641f8e610
commit a3ed4886a3
22 changed files with 2450 additions and 20 deletions

View file

@ -0,0 +1,220 @@
import {
CreateTemplate,
CreateTemplateGroup,
CreateTemplateGroupResponse,
ImportTemplateResponse,
InputMaybe
} from "../schema/generated/graphql.js";
import {logger} from "../logging/logger.js";
import {
ZabbixCreateItemRequest,
ZabbixCreateTemplateGroupRequest,
ZabbixCreateTemplateRequest,
ZabbixQueryItemRequest,
ZabbixQueryTemplateGroupRequest,
ZabbixQueryTemplatesRequest
} from "../datasources/zabbix-templates.js";
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js";
import {zabbixAPI} from "../datasources/zabbix-api.js";
export class TemplateImporter {
public static async importTemplateGroups(templateGroups: InputMaybe<Array<CreateTemplateGroup>> | undefined, zabbixAuthToken?: string, cookie?: string) {
if (!templateGroups) {
return null
}
let result: CreateTemplateGroupResponse[] = []
for (let group of templateGroups) {
let createGroupResult: { groupids: string[] } | ZabbixErrorResult | undefined = undefined;
// Try to find if it exists by name first
let groups = await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter_name: group.groupName
}))
let groupid = 0
let message: string | undefined = undefined
if (!isZabbixErrorResult(groups) && groups?.length) {
groupid = Number(groups[0].groupid)
message = `Template group ${group.groupName} already exists with groupid=${groupid} - skipping`
logger.debug(message)
} else {
createGroupResult = await new ZabbixCreateTemplateGroupRequest(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
name: group.groupName,
uuid: group.uuid
}))
if (isZabbixErrorResult(createGroupResult)) {
let errorMessage = createGroupResult.error.message;
if (createGroupResult.error.data) {
errorMessage += " " + (typeof createGroupResult.error.data === 'string' ? createGroupResult.error.data : JSON.stringify(createGroupResult.error.data));
}
result.push({
groupName: group.groupName,
message: `Unable to create template group ${group.groupName}: ${errorMessage}`,
error: createGroupResult.error
})
continue
} else if (createGroupResult?.groupids?.length) {
groupid = Number(createGroupResult.groupids[0])
}
}
if (groupid) {
result.push({
groupName: group.groupName,
groupid: groupid,
message: message
})
} else {
result.push({
groupName: group.groupName,
message: `Unable to create template group ${group.groupName}: Unknown error`,
error: { message: "Unknown error - no groupid returned" }
})
}
}
return result
}
public static async importTemplates(templates: InputMaybe<Array<CreateTemplate>> | undefined, zabbixAuthToken?: string, cookie?: string) {
if (!templates) {
return null
}
let result: ImportTemplateResponse[] = []
for (let template of templates) {
// 1. Resolve Group IDs
let groupids = template.groupids
if (!groupids || groupids.length === 0) {
let groups = await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter_name: template.groupNames
}))
if (isZabbixErrorResult(groups) || !groups?.length) {
result.push({
host: template.host,
message: `Unable to find template groups=${template.groupNames}`
})
continue
}
groupids = groups.map(g => Number(g.groupid))
}
// 2. Resolve Linked Templates IDs
let linkedTemplates: { templateid: string }[] = []
if (template.templates && template.templates.length > 0) {
let templateNames = template.templates.map(t => t.name)
let queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter_host: templateNames
}))
if (isZabbixErrorResult(queryResult)) {
let errorMessage = queryResult.error.message;
if (queryResult.error.data) {
errorMessage += " " + (typeof queryResult.error.data === 'string' ? queryResult.error.data : JSON.stringify(queryResult.error.data));
}
result.push({
host: template.host,
message: `Error querying linked templates: ${errorMessage}`,
error: queryResult.error
})
continue
}
linkedTemplates = queryResult.map(t => ({ templateid: t.templateid }))
}
// 3. Create Template
let templateCreateParams: any = {
host: template.host,
name: template.name || template.host,
groups: groupids.map(id => ({ groupid: id })),
uuid: template.uuid,
templates: linkedTemplates,
tags: template.tags?.map(t => ({ tag: t.tag, value: t.value || "" }))
}
let templateImportResult = await new ZabbixCreateTemplateRequest(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs(templateCreateParams))
if (isZabbixErrorResult(templateImportResult) || !templateImportResult?.templateids?.length) {
let errorMessage = isZabbixErrorResult(templateImportResult) ? templateImportResult.error.message : "Unknown error";
if (isZabbixErrorResult(templateImportResult) && templateImportResult.error.data) {
errorMessage += " " + (typeof templateImportResult.error.data === 'string' ? templateImportResult.error.data : JSON.stringify(templateImportResult.error.data));
}
result.push({
host: template.host,
message: `Unable to import template=${template.host}: ${errorMessage}`,
error: isZabbixErrorResult(templateImportResult) ? templateImportResult.error : undefined
})
continue
}
let templateid = templateImportResult.templateids[0]
// 4. Create Items if any
if (template.items && template.items.length > 0) {
const createdItemKeyToId = new Map<string, string>();
let itemsToCreate = [...template.items];
let retry = true;
while (retry && itemsToCreate.length > 0) {
retry = false;
const remainingItems: typeof itemsToCreate = [];
for (let item of itemsToCreate) {
if (item.master_item && !createdItemKeyToId.has(item.master_item.key)) {
remainingItems.push(item);
continue;
}
let { key, master_item, ...itemData } = item;
let itemCreateParams: any = {
...itemData,
key_: key,
hostid: templateid,
preprocessing: item.preprocessing?.map(p => ({
type: p.type,
params: p.params.join("\n"),
error_handler: p.error_handler,
error_handler_params: p.error_handler_params
})),
tags: item.tags?.map(t => ({ tag: t.tag, value: t.value || "" }))
}
if (master_item) {
itemCreateParams.master_itemid = createdItemKeyToId.get(master_item.key);
}
let itemResult = await new ZabbixCreateItemRequest(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs(itemCreateParams))
if (isZabbixErrorResult(itemResult)) {
let errorMessage = itemResult.error.message;
if (itemResult.error.data) {
errorMessage += " " + (typeof itemResult.error.data === 'string' ? itemResult.error.data : JSON.stringify(itemResult.error.data));
}
logger.error(`Unable to create item ${item.name} for template ${template.host}: ${errorMessage}`)
} else if (itemResult?.itemids?.length) {
createdItemKeyToId.set(key, itemResult.itemids[0]);
retry = true;
}
}
itemsToCreate = remainingItems;
}
if (itemsToCreate.length > 0) {
logger.error(`Unable to create ${itemsToCreate.length} items for template ${template.host} due to missing master items: ${itemsToCreate.map(i => i.name).join(", ")}`);
}
}
result.push({
host: template.host,
templateid: templateid
})
}
return result
}
}