This commit introduces two new device types, GroundValueChecker and WeatherSensorDevice, which leverage public APIs (BORIS NRW and Open-Meteo) for real-time data collection. It also includes several API enhancements and fixes to support these new integrations. Detailed changes: - **New Device Types**: - Added GroundValueChecker schema and integration with BORIS NRW WMS via Zabbix Script items. - Added WeatherSensorDevice schema and integration with Open-Meteo via Zabbix HTTP Agent items. - **API Enhancements**: - Added error field to ZabbixItem for item-level error reporting. - Updated CreateTemplateItem mutation input to support params (for Script items) and timeout. - Registered missing scalar resolvers: JSONObject, DateTime, and Time. - **Performance & Reliability**: - Implemented batch fetching for item preprocessing in both host and template queries to reduce Zabbix API calls and ensure data visibility. - Updated template_importer.ts to correctly handle Script item parameters. - **Documentation**: - Consolidated public API device recipes in docs/howtos/cookbook.md. - Added guidance on analyzing data update frequency and setting reasonable update intervals (e.g., 1h for weather, 1d for ground values). - **Testing**: - Added new regression test REG-ITEM-META to verify item metadata (units, description, error, preprocessing) and JSONObject scalar support. - Enhanced RegressionTestExecutor with more detailed host-item relationship verification.
316 lines
16 KiB
TypeScript
316 lines
16 KiB
TypeScript
import {SmoketestResponse, SmoketestStep} from "../schema/generated/graphql.js";
|
|
import {HostImporter} from "./host_importer.js";
|
|
import {HostDeleter} from "./host_deleter.js";
|
|
import {TemplateImporter} from "./template_importer.js";
|
|
import {TemplateDeleter} from "./template_deleter.js";
|
|
import {logger} from "../logging/logger.js";
|
|
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
|
import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js";
|
|
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
|
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
|
|
|
export class RegressionTestExecutor {
|
|
public static async runAllRegressionTests(hostName: string, groupName: string, zabbixAuthToken?: string, cookie?: string): Promise<SmoketestResponse> {
|
|
const steps: SmoketestStep[] = [];
|
|
let success = true;
|
|
|
|
try {
|
|
// Regression 1: Locations query argument order
|
|
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
|
|
try {
|
|
const locations = await zabbixAPI.getLocations(new ParsedArgs({ name_pattern: "NonExistent_" + Math.random() }), zabbixAuthToken, cookie);
|
|
steps.push({
|
|
name: "REG-LOC: Locations query argument order",
|
|
success: true,
|
|
message: "Locations query executed without session error"
|
|
});
|
|
} catch (error: any) {
|
|
steps.push({
|
|
name: "REG-LOC: Locations query argument order",
|
|
success: false,
|
|
message: `Failed: ${error.message}`
|
|
});
|
|
success = false;
|
|
}
|
|
|
|
// Regression 2: Template lookup by technical name
|
|
// Verifies that importHosts can link templates using their technical name (host)
|
|
const regTemplateName = "REG_TEMP_" + Math.random().toString(36).substring(7);
|
|
const regGroupName = "Templates/Roadwork/Devices";
|
|
const hostGroupName = "Roadwork/Devices";
|
|
|
|
const tempResult = await TemplateImporter.importTemplates([{
|
|
host: regTemplateName,
|
|
name: "Regression Test Template",
|
|
groupNames: [regGroupName]
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const tempSuccess = !!tempResult?.length && !tempResult[0].error;
|
|
steps.push({
|
|
name: "REG-TEMP: Template technical name lookup",
|
|
success: tempSuccess,
|
|
message: tempSuccess ? `Template ${regTemplateName} created and searchable by technical name` : `Failed to create template`
|
|
});
|
|
if (!tempSuccess) success = false;
|
|
|
|
// Regression 3: HTTP Agent URL support
|
|
// Verifies that templates with HTTP Agent items (including URL) can be imported
|
|
const httpTempName = "REG_HTTP_" + Math.random().toString(36).substring(7);
|
|
const httpTempResult = await TemplateImporter.importTemplates([{
|
|
host: httpTempName,
|
|
name: "Regression HTTP Template",
|
|
groupNames: [regGroupName],
|
|
items: [{
|
|
name: "HTTP Master",
|
|
type: 19, // HTTP Agent
|
|
key: "http.master",
|
|
value_type: 4,
|
|
url: "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=temperature_2m",
|
|
delay: "1m",
|
|
history: "0"
|
|
}]
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const httpSuccess = !!httpTempResult?.length && !httpTempResult[0].error;
|
|
steps.push({
|
|
name: "REG-HTTP: HTTP Agent URL support",
|
|
success: httpSuccess,
|
|
message: httpSuccess ? `Template ${httpTempName} with HTTP Agent item created successfully` : `Failed: ${httpTempResult?.[0]?.message}`
|
|
});
|
|
if (!httpSuccess) success = false;
|
|
|
|
// Regression 4: User Macro assignment for host and template creation
|
|
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
|
|
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
|
|
|
|
const macroTempResult = await TemplateImporter.importTemplates([{
|
|
host: macroTemplateName,
|
|
name: "Regression Macro Template",
|
|
groupNames: [regGroupName],
|
|
macros: [
|
|
{ macro: "{$TEMP_MACRO}", value: "temp_value" }
|
|
]
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const macroTempImportSuccess = !!macroTempResult?.length && !macroTempResult[0].error;
|
|
let macroHostImportSuccess = false;
|
|
let macroVerifySuccess = false;
|
|
|
|
if (macroTempImportSuccess) {
|
|
const macroHostResult = await HostImporter.importHosts([{
|
|
deviceKey: macroHostName,
|
|
deviceType: "RegressionHost",
|
|
groupNames: [hostGroupName],
|
|
templateNames: [macroTemplateName],
|
|
macros: [
|
|
{ macro: "{$HOST_MACRO}", value: "host_value" }
|
|
]
|
|
}], zabbixAuthToken, cookie);
|
|
macroHostImportSuccess = !!macroHostResult?.length && !!macroHostResult[0].hostid;
|
|
|
|
if (macroHostImportSuccess) {
|
|
// Verify macros on host
|
|
const verifyHostResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
|
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
|
filter_host: macroHostName,
|
|
selectMacros: "extend"
|
|
}));
|
|
|
|
// Verify macros on template
|
|
const verifyTempResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
|
filter_host: macroTemplateName,
|
|
selectMacros: "extend"
|
|
}));
|
|
|
|
const hasHostMacro = Array.isArray(verifyHostResult) && verifyHostResult.length > 0 &&
|
|
(verifyHostResult[0] as any).macros?.some((m: any) => m.macro === "{$HOST_MACRO}" && m.value === "host_value");
|
|
|
|
const hasTempMacro = Array.isArray(verifyTempResult) && verifyTempResult.length > 0 &&
|
|
(verifyTempResult[0] as any).macros?.some((m: any) => m.macro === "{$TEMP_MACRO}" && m.value === "temp_value");
|
|
|
|
macroVerifySuccess = !!(hasHostMacro && hasTempMacro);
|
|
}
|
|
}
|
|
|
|
const macroOverallSuccess = macroTempImportSuccess && macroHostImportSuccess && macroVerifySuccess;
|
|
steps.push({
|
|
name: "REG-MACRO: User Macro assignment",
|
|
success: macroOverallSuccess,
|
|
message: macroOverallSuccess
|
|
? "Macros successfully assigned to template and host"
|
|
: `Failed: TempImport=${macroTempImportSuccess}, HostImport=${macroHostImportSuccess}, Verify=${macroVerifySuccess}`
|
|
});
|
|
if (!macroOverallSuccess) success = false;
|
|
|
|
// Regression 5: Host retrieval and visibility (allHosts output fields fix)
|
|
if (success) {
|
|
const hostResult = await HostImporter.importHosts([{
|
|
deviceKey: hostName,
|
|
deviceType: "RegressionHost",
|
|
groupNames: [hostGroupName],
|
|
templateNames: [regTemplateName]
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const hostImportSuccess = !!hostResult?.length && !!hostResult[0].hostid;
|
|
if (hostImportSuccess) {
|
|
const hostid = hostResult[0].hostid;
|
|
logger.info(`REG-HOST: Host ${hostName} imported with ID ${hostid}. Verifying visibility...`);
|
|
|
|
// Verify visibility via allHosts (simulated)
|
|
const verifyResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
|
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
|
filter_host: hostName
|
|
}));
|
|
|
|
const verified = Array.isArray(verifyResult) && verifyResult.length > 0 && (verifyResult[0] as any).host === hostName;
|
|
|
|
let fieldsVerified = false;
|
|
if (verified) {
|
|
const host = verifyResult[0] as any;
|
|
const hasGroups = Array.isArray(host.hostgroups) && host.hostgroups.length > 0;
|
|
const hasTemplates = Array.isArray(host.parentTemplates) && host.parentTemplates.length > 0;
|
|
fieldsVerified = hasGroups && hasTemplates;
|
|
|
|
if (!fieldsVerified) {
|
|
logger.error(`REG-HOST: Fields verification failed. Groups: ${hasGroups}, Templates: ${hasTemplates}. Host data: ${JSON.stringify(host)}`);
|
|
}
|
|
}
|
|
|
|
if (!verified) {
|
|
logger.error(`REG-HOST: Verification failed. Zabbix result: ${JSON.stringify(verifyResult)}`);
|
|
}
|
|
steps.push({
|
|
name: "REG-HOST: Host retrieval and visibility (incl. groups and templates)",
|
|
success: verified && fieldsVerified,
|
|
message: verified
|
|
? (fieldsVerified ? `Host ${hostName} retrieved successfully with groups and templates` : `Host ${hostName} retrieved but missing groups or templates`)
|
|
: "Host not found after import (output fields issue?)"
|
|
});
|
|
if (!verified || !fieldsVerified) success = false;
|
|
} else {
|
|
steps.push({
|
|
name: "REG-HOST: Host retrieval and visibility",
|
|
success: false,
|
|
message: `Host import failed: ${hostResult?.[0]?.message || hostResult?.[0]?.error?.message || "Unknown error"}`
|
|
});
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
// Regression 6: Item Metadata (preprocessing, units, description, error)
|
|
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
|
|
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
|
|
|
|
const metaTempResult = await TemplateImporter.importTemplates([{
|
|
host: metaTempName,
|
|
name: "Regression Meta Template",
|
|
groupNames: [regGroupName],
|
|
items: [{
|
|
name: "Meta Item",
|
|
type: 2, // Zabbix trapper
|
|
key: "meta.item",
|
|
value_type: 0, // Float
|
|
units: "TEST_UNIT",
|
|
description: "Test Description",
|
|
history: "1d",
|
|
preprocessing: [
|
|
{
|
|
type: 12, // JSONPath
|
|
params: ["$.value"]
|
|
}
|
|
]
|
|
}]
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const metaTempSuccess = !!metaTempResult?.length && !metaTempResult[0].error;
|
|
let metaHostSuccess = false;
|
|
let metaVerifySuccess = false;
|
|
|
|
if (metaTempSuccess) {
|
|
const metaHostResult = await HostImporter.importHosts([{
|
|
deviceKey: metaHostName,
|
|
deviceType: "RegressionHost",
|
|
groupNames: [hostGroupName],
|
|
templateNames: [metaTempName]
|
|
}], zabbixAuthToken, cookie);
|
|
metaHostSuccess = !!metaHostResult?.length && !!metaHostResult[0].hostid;
|
|
|
|
if (metaHostSuccess) {
|
|
// Verify item metadata
|
|
const verifyResult = await new ZabbixQueryHostsGenericRequestWithItems("host.get", zabbixAuthToken, cookie)
|
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
|
filter_host: metaHostName
|
|
}));
|
|
|
|
if (Array.isArray(verifyResult) && verifyResult.length > 0) {
|
|
const host = verifyResult[0] as any;
|
|
const item = host.items?.find((i: any) => i.key_ === "meta.item");
|
|
|
|
if (item) {
|
|
const hasUnits = item.units === "TEST_UNIT";
|
|
const hasDesc = item.description === "Test Description";
|
|
// Zabbix might return type as string or number depending on version/API, but usually it's string in JSON result if not cast
|
|
const hasPreproc = Array.isArray(item.preprocessing) && item.preprocessing.length > 0 &&
|
|
String(item.preprocessing[0].type) === "12";
|
|
const hasErrorField = item.hasOwnProperty("error");
|
|
|
|
metaVerifySuccess = hasUnits && hasDesc && hasPreproc && hasErrorField;
|
|
|
|
if (!metaVerifySuccess) {
|
|
logger.error(`REG-META: Verification failed. Units: ${hasUnits}, Desc: ${hasDesc}, Preproc: ${hasPreproc}, ErrorField: ${hasErrorField}. Item: ${JSON.stringify(item)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const metaOverallSuccess = metaTempSuccess && metaHostSuccess && metaVerifySuccess;
|
|
steps.push({
|
|
name: "REG-ITEM-META: Item metadata (preprocessing, units, description, error)",
|
|
success: metaOverallSuccess,
|
|
message: metaOverallSuccess
|
|
? "Item metadata successfully retrieved including preprocessing and units"
|
|
: `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}`
|
|
});
|
|
if (!metaOverallSuccess) success = false;
|
|
|
|
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
|
const groupResult = await HostImporter.importHostGroups([{
|
|
groupName: groupName
|
|
}], zabbixAuthToken, cookie);
|
|
|
|
const groupSuccess = !!groupResult?.length && !groupResult[0].error;
|
|
steps.push({
|
|
name: "Create Host Group",
|
|
success: groupSuccess,
|
|
message: groupSuccess ? `Host group ${groupName} created` : `Failed: ${groupResult?.[0]?.error?.message || "Unknown error"}`
|
|
});
|
|
if (!groupSuccess) success = false;
|
|
|
|
// Cleanup
|
|
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
|
|
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
|
|
await HostDeleter.deleteHosts(null, metaHostName, zabbixAuthToken, cookie);
|
|
await TemplateDeleter.deleteTemplates(null, regTemplateName, zabbixAuthToken, cookie);
|
|
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
|
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
|
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
|
|
// We don't delete the group here as it might be shared or used by other tests in this run
|
|
|
|
} catch (error: any) {
|
|
success = false;
|
|
steps.push({
|
|
name: "Execution Error",
|
|
success: false,
|
|
message: error.message || String(error)
|
|
});
|
|
}
|
|
|
|
return {
|
|
success,
|
|
message: success ? "Regression tests passed successfully" : "Regression tests failed",
|
|
steps
|
|
};
|
|
}
|
|
}
|