feat: add GroundValueChecker and WeatherSensorDevice with public API integration

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.
This commit is contained in:
Andreas Hilbig 2026-02-01 21:07:21 +01:00
parent 41e4c4da1f
commit ad104acde2
13 changed files with 378 additions and 45 deletions

View file

@ -194,17 +194,18 @@ AI agents can use the generalized `verifySchemaExtension.graphql` operations to
--- ---
## 🍳 Recipe: Extending Schema with a Weather Sensor Device (Public API) ## 🍳 Recipe: Extending Schema with Public API Devices
This recipe demonstrates how to extend the schema with a new device type that retrieves real-time weather data from a public API (Open-Meteo) using Zabbix HTTP agent items. This approach allows you to integrate external data sources into your Zabbix monitoring and expose them through the GraphQL API. This recipe demonstrates how to extend the schema with new device types that retrieve data from public APIs (e.g. Weather or Ground Values) using Zabbix HTTP agent or Script items. This approach allows you to integrate external data sources and expose them through the GraphQL API.
### 📋 Prerequisites ### 📋 Prerequisites
- Zabbix GraphQL API is running. - Zabbix GraphQL API is running.
- The device has geo-coordinates set via user macros (`{$LAT}` and `{$LON}`). - The device has geo-coordinates set via user macros (e.g. `{$LAT}` and `{$LON}`).
### 🛠️ Step 1: Define the Schema Extension ### 🛠️ Step 1: Define the Schema Extension
Create a new `.graphql` file in `schema/extensions/` named `weather_sensor.graphql`. Create a new `.graphql` file in `schema/extensions/` (e.g. `weather_sensor.graphql` or `ground_value_checker.graphql`).
**Sample: Weather Sensor**
```graphql ```graphql
type WeatherSensorDevice implements Host & Device { type WeatherSensorDevice implements Host & Device {
hostid: ID! hostid: ID!
@ -222,34 +223,83 @@ type WeatherSensorState implements DeviceState {
} }
type WeatherSensorValues { type WeatherSensorValues {
"""
Current temperature at the device location (in °C).
"""
temperature: Float temperature: Float
"""
Warnings or description of the street conditions (e.g. Ice, Rain, Clear). Derived from Open-Meteo weather codes.
"""
streetConditionWarnings: String streetConditionWarnings: String
} }
``` ```
**Sample: Ground Value Checker**
```graphql
type GroundValueChecker implements Host & Device {
hostid: ID!
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: DeviceConfig
state: GroundValueState
}
type GroundValueState implements DeviceState {
operational: OperationalDeviceData
current: GroundValues
}
type GroundValues {
"""
Average ground value (in €/m²). Extracted from the BORIS NRW GeoJSON response.
"""
averageValue: Float
}
```
### ⚙️ Step 2: Register the Resolver ### ⚙️ Step 2: Register the Resolver
Add the new type and schema to your `.env` file to enable the dynamic resolver: Add the new types and schemas to your `.env` file to enable the dynamic resolver:
```env ```env
ADDITIONAL_SCHEMAS=./schema/extensions/weather_sensor.graphql ADDITIONAL_SCHEMAS=./schema/extensions/weather_sensor.graphql,./schema/extensions/ground_value_checker.graphql
ADDITIONAL_RESOLVERS=WeatherSensorDevice ADDITIONAL_RESOLVERS=WeatherSensorDevice,GroundValueChecker
``` ```
Restart the API server to apply the changes. Restart the API server to apply the changes.
### 🚀 Step 3: Import the Weather Sensor Template ### 📊 Step 3: Analyse Data Update Frequency
Use the `importTemplates` mutation to create the `WEATHER_SENSOR` template. This template uses an **HTTP agent** item to fetch data from Open-Meteo and **dependent items** to parse the results. Before configuring the Zabbix template, analyse how often the data source actually updates. Setting an update interval (`delay`) that is too frequent puts unnecessary load on public APIs and your Zabbix server.
> **Reference**: See the [Sample: Weather Sensor Template Import](../../docs/queries/sample_import_weather_sensor_template.graphql) for the complete mutation and variables. - **Dynamic Data (e.g. Weather)**: Weather data typically updates every 15 to 60 minutes. A `delay` of `1h` or `30m` is usually sufficient for monitoring purposes.
- **Static Data (e.g. Ground Values)**: Ground values (Bodenrichtwerte) are typically updated once a year. A `delay` of `1d` or even `7d` is more than enough.
**Key Item Configuration**: ### 🚀 Step 4: Import the Device Template
Use the `importTemplates` mutation to create the template. Use **HTTP agent** or **Script** items as master items to fetch data, and **dependent items** to parse the results.
> **Reference**: See the [Sample: Weather Sensor Template Import](../../docs/queries/sample_import_weather_sensor_template.graphql) and [Sample: Ground Value Checker Template Import](../../docs/queries/sample_import_ground_value_checker_template.graphql) for complete mutations.
**Key Configuration for Weather Sensor**:
- **Master Item**: `weather.get` (HTTP Agent) - **Master Item**: `weather.get` (HTTP Agent)
- URL: `https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}&current=temperature_2m,weather_code` - URL: `https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}&current=temperature_2m,weather_code`
- **Delay**: `1h` (Reasonable for weather data)
- Description: Master item fetching weather data from Open-Meteo based on host coordinates.
- **Dependent Item**: `state.current.temperature` (JSONPath: `$.current.temperature_2m`) - **Dependent Item**: `state.current.temperature` (JSONPath: `$.current.temperature_2m`)
- **Dependent Item**: `state.current.streetConditionWarnings` (JavaScript mapping from `$.current.weather_code`) - Units: `°C`
- Description: The current temperature at the device location.
### ✅ Step 4: Verification **Key Configuration for Ground Value Checker**:
Create a host, assign it macros for coordinates, and query its weather state. - **Master Item**: `boris.get` (Script)
- Params: JavaScript snippet for BBOX calculation and `HttpRequest`.
- **Delay**: `1d` (Reasonable for ground values)
- Description: Script item calculating BBOX and fetching ground values from BORIS NRW.
- **Dependent Item**: `state.current.averageValue` (JSONPath: `$.features[0].properties.Bodenrichtwert`)
- Units: `€/m²`
- Description: The average ground value (Bodenrichtwert) extracted from the BORIS NRW GeoJSON response.
1. **Create Host**: ### ✅ Step 5: Verification
Create a host, assign it macros for coordinates, and query its state.
1. **Create Host (Weather Example)**:
```graphql ```graphql
mutation CreateWeatherHost { mutation CreateWeatherHost {
importHosts(hosts: [{ importHosts(hosts: [{
@ -261,9 +311,7 @@ Create a host, assign it macros for coordinates, and query its weather state.
{ macro: "{$LAT}", value: "52.52" }, { macro: "{$LAT}", value: "52.52" },
{ macro: "{$LON}", value: "13.41" } { macro: "{$LON}", value: "13.41" }
], ],
location: { location: { name: "Berlin" }
name: "Berlin"
}
}]) { }]) {
hostid hostid
} }
@ -272,10 +320,10 @@ Create a host, assign it macros for coordinates, and query its weather state.
2. **Query Data**: 2. **Query Data**:
```graphql ```graphql
query GetWeather { query GetDeviceState {
allDevices(tag_deviceType: ["WeatherSensorDevice"]) { allDevices(tag_deviceType: ["WeatherSensorDevice", "GroundValueChecker"]) {
name
... on WeatherSensorDevice { ... on WeatherSensorDevice {
name
state { state {
current { current {
temperature temperature
@ -283,6 +331,13 @@ Create a host, assign it macros for coordinates, and query its weather state.
} }
} }
} }
... on GroundValueChecker {
state {
current {
averageValue
}
}
}
} }
} }
``` ```

View file

@ -0,0 +1,69 @@
### Mutation
Use this mutation to import a template specifically designed to work with the `GroundValueChecker` type. This template uses a Zabbix Script item to dynamically calculate a BBOX around the host's coordinates and fetch ground values from the NRW BORIS WMS service.
```graphql
mutation ImportGroundValueTemplate($templates: [CreateTemplate!]!) {
importTemplates(templates: $templates) {
host
templateid
message
error {
message
code
}
}
}
```
### Variables
The following variables define the `GROUND_VALUE_CHECKER` template. It uses the host's user macros (`{$LAT}` and `{$LON}`) to fetch localized ground value data.
```json
{
"templates": [
{
"host": "GROUND_VALUE_CHECKER",
"name": "Ground Value Checker Template",
"groupNames": ["Templates/External APIs"],
"tags": [
{ "tag": "deviceType", "value": "GroundValueChecker" }
],
"macros": [
{ "macro": "{$LAT}", "value": "51.22" },
{ "macro": "{$LON}", "value": "6.77" }
],
"items": [
{
"name": "BORIS NRW API Fetch",
"type": 21,
"key": "boris.get",
"value_type": 4,
"history": "0",
"delay": "1d",
"params": "var lat = '{$LAT}';\nvar lon = '{$LON}';\n\nif (lat.indexOf('{$') === 0 || lon.indexOf('{$') === 0) {\n throw 'Macros {$LAT} and {$LON} must be set on the host.';\n}\n\nlat = parseFloat(lat);\nlon = parseFloat(lon);\n\nvar delta = 0.0005; \nvar minLon = lon - delta;\nvar minLat = lat - delta;\nmaxLon = lon + delta;\nmaxLat = lat + delta;\n\nvar bbox = minLon.toFixed(6) + \",\" + minLat.toFixed(6) + \",\" + maxLon.toFixed(6) + \",\" + maxLat.toFixed(6);\n\nvar url = \"https://www.wms.nrw.de/boris/wms_de_bodenrichtwerte?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=brw_wohnbauflaeche,brw_gewerbliche_bauweise,brw_gemischte_bauweise&QUERY_LAYERS=brw_wohnbauflaeche,brw_gewerbliche_bauweise,brw_gemischte_bauweise&I=50&J=50&WIDTH=101&HEIGHT=101&CRS=CRS:84&BBOX=\" + bbox + \"&INFO_FORMAT=application/geo%2Bjson\";\n\nvar request = new HttpRequest();\nrequest.addHeader('Accept: application/geo+json');\nvar response = request.get(url);\n\nif (request.getStatus() !== 200) {\n throw 'Response code: ' + request.getStatus();\n}\n\nreturn response;",
"timeout": "10s",
"description": "Script item calculating BBOX and fetching ground values from BORIS NRW."
},
{
"name": "Average Ground Value",
"type": 18,
"key": "state.current.averageValue",
"value_type": 0,
"history": "7d",
"units": "€/m²",
"description": "The average ground value (Bodenrichtwert) extracted from the BORIS NRW GeoJSON response.",
"master_item": {
"key": "boris.get"
},
"preprocessing": [
{
"type": 12,
"params": ["$.features[0].properties.Bodenrichtwert"]
}
]
}
]
}
]
}
```

View file

@ -39,7 +39,7 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
"key": "weather.get", "key": "weather.get",
"value_type": 4, "value_type": 4,
"history": "0", "history": "0",
"delay": "1m", "delay": "1h",
"url": "https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}&current=temperature_2m,weather_code", "url": "https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}&current=temperature_2m,weather_code",
"description": "Master item fetching weather data from Open-Meteo based on host coordinates." "description": "Master item fetching weather data from Open-Meteo based on host coordinates."
}, },
@ -49,6 +49,8 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
"key": "state.current.temperature", "key": "state.current.temperature",
"value_type": 0, "value_type": 0,
"history": "7d", "history": "7d",
"units": "°C",
"description": "The current temperature at the device location.",
"master_item": { "master_item": {
"key": "weather.get" "key": "weather.get"
}, },
@ -65,6 +67,7 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
"key": "state.current.streetConditionWarnings", "key": "state.current.streetConditionWarnings",
"value_type": 4, "value_type": 4,
"history": "7d", "history": "7d",
"description": "Human-readable weather warnings or descriptions derived from weather codes.",
"master_item": { "master_item": {
"key": "weather.get" "key": "weather.get"
}, },

View file

@ -0,0 +1,46 @@
"""
GroundValueChecker represents a device that retrieves ground values
from public APIs (e.g. BORIS NRW) using Zabbix HTTP agent items.
"""
type GroundValueChecker implements Host & Device {
"""Internal Zabbix ID of the device."""
hostid: ID!
"""
Per convention a uuid is used as hostname to identify devices if they do not have a unique hostname.
"""
host: String!
"""Classification of the device."""
deviceType: String
"""List of host groups this device belongs to."""
hostgroups: [HostGroup!]
"""Visible name of the device."""
name: String
"""Device configuration tags."""
tags: DeviceConfig
"""Host inventory data."""
inventory: Inventory
"""List of monitored items for this host."""
items: [ZabbixItem!]
"""State of the ground value checker device."""
state: GroundValueState
}
"""
Represents the state of a ground value checker device.
"""
type GroundValueState implements DeviceState {
"""Operational data (telemetry)."""
operational: OperationalDeviceData
"""Current business values (ground data)."""
current: GroundValues
}
"""
Aggregated ground information retrieved from the API.
"""
type GroundValues {
"""
Average ground value (in /m²). Extracted from the BORIS NRW GeoJSON response.
"""
averageValue: Float
}

View file

@ -40,11 +40,11 @@ Aggregated weather information retrieved from the API.
""" """
type WeatherSensorValues { type WeatherSensorValues {
""" """
Current temperature at the device location (in Celsius). Current temperature at the device location (in °C).
""" """
temperature: Float temperature: Float
""" """
Warnings or description of the street conditions (e.g. Ice, Rain, Clear). Warnings or description of the street conditions (e.g. Ice, Rain, Clear). Derived from Open-Meteo weather codes.
""" """
streetConditionWarnings: String streetConditionWarnings: String
} }

View file

@ -313,6 +313,14 @@ input CreateTemplateItem {
""" """
url: String url: String
""" """
JavaScript code for Script items or other parameters.
"""
params: String
"""
Timeout for item data collection.
"""
timeout: String
"""
Preprocessing steps for the item values. Preprocessing steps for the item values.
""" """
preprocessing: [CreateItemPreprocessing!] preprocessing: [CreateItemPreprocessing!]

View file

@ -80,6 +80,10 @@ type ZabbixItem {
""" """
lastvalue: String lastvalue: String
""" """
Error message if the item is in an error state.
"""
error: String
"""
Type of information (e.g. 0 for Float, 3 for Int, 4 for Text). Type of information (e.g. 0 for Float, 3 for Int, 4 for Text).
""" """
value_type: Int! value_type: Int!

View file

@ -24,6 +24,7 @@ import {
StorageItemType, StorageItemType,
} from "../schema/generated/graphql.js"; } from "../schema/generated/graphql.js";
import { DateTimeResolver, JSONObjectResolver, TimeResolver } from "graphql-scalars";
import {HostImporter} from "../execution/host_importer.js"; import {HostImporter} from "../execution/host_importer.js";
import {HostDeleter} from "../execution/host_deleter.js"; import {HostDeleter} from "../execution/host_deleter.js";
import {SmoketestExecutor} from "../execution/smoketest_executor.js"; import {SmoketestExecutor} from "../execution/smoketest_executor.js";
@ -67,6 +68,9 @@ export function createResolvers(): Resolvers {
// @ts-ignore // @ts-ignore
// @ts-ignore // @ts-ignore
return { return {
DateTime: DateTimeResolver,
Time: TimeResolver,
JSONObject: JSONObjectResolver,
Query: { Query: {
userPermissions: async (_parent: any, objectNamesFilter: QueryUserPermissionsArgs, { userPermissions: async (_parent: any, objectNamesFilter: QueryUserPermissionsArgs, {
zabbixAuthToken, zabbixAuthToken,

View file

@ -9,6 +9,7 @@ import {
ZabbixResult ZabbixResult
} from "./zabbix-request.js"; } from "./zabbix-request.js";
import {ZabbixHistoryGetParams, ZabbixQueryHistoryRequest} from "./zabbix-history.js"; import {ZabbixHistoryGetParams, ZabbixQueryHistoryRequest} from "./zabbix-history.js";
import {ZabbixQueryItemRequest} from "./zabbix-templates.js";
export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> { export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> {
@ -80,6 +81,14 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
"type", "type",
"value_type", "value_type",
"status", "status",
"error",
"units",
"history",
"delay",
"description",
"preprocessing",
"tags",
"master_itemid",
], ],
output: [ output: [
"hostid", "hostid",
@ -97,26 +106,44 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
let result = await super.executeRequestReturnError(zabbixAPI, args); let result = await super.executeRequestReturnError(zabbixAPI, args);
if (result && !isZabbixErrorResult(result)) { if (result && !isZabbixErrorResult(result)) {
for (let device of <ZabbixHost[]>result) { const hosts = <ZabbixHost[]>result;
for (let item of device.items || []) { const hostids = hosts.map(h => h.hostid);
if (!item.lastclock ) {
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError( if (hostids.length > 0) {
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type)) // Batch fetch preprocessing for all items of these hosts
if (isZabbixErrorResult(values)) { const allItems = await new ZabbixQueryItemRequest(this.authToken, this.cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
return values; hostids: hostids,
} selectPreprocessing: "extend"
if (values.length) { }));
let latestValue = values[0];
item.lastvalue = latestValue.value; if (!isZabbixErrorResult(allItems) && Array.isArray(allItems)) {
item.lastclock = latestValue.clock; const itemidToPreprocessing = new Map<string, any>();
} else { allItems.forEach((item: any) => {
item.lastvalue = null; itemidToPreprocessing.set(item.itemid, item.preprocessing);
item.lastclock = null; });
for (let device of hosts) {
for (let item of device.items || []) {
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
if (!item.lastclock ) {
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
if (isZabbixErrorResult(values)) {
return values;
}
if (values.length) {
let latestValue = values[0];
item.lastvalue = latestValue.value;
item.lastclock = latestValue.clock;
} else {
item.lastvalue = null;
item.lastclock = null;
}
}
} }
} }
} }
} }
} }
return result; return result;

View file

@ -1,4 +1,4 @@
import {ZabbixRequest, ParsedArgs, isZabbixErrorResult, ZabbixParams} from "./zabbix-request.js"; import {ZabbixRequest, ParsedArgs, isZabbixErrorResult, ZabbixParams, ZabbixErrorResult} from "./zabbix-request.js";
import {ZabbixAPI} from "./zabbix-api.js"; import {ZabbixAPI} from "./zabbix-api.js";
import {logger} from "../logging/logger.js"; import {logger} from "../logging/logger.js";
@ -24,6 +24,36 @@ export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTempla
...args?.zabbix_params ...args?.zabbix_params
}; };
} }
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
let result = await super.executeRequestReturnError(zabbixAPI, args);
if (result && !isZabbixErrorResult(result) && Array.isArray(result)) {
const templateids = result.map(t => t.templateid);
if (templateids.length > 0) {
// Batch fetch preprocessing for all items of these templates
const allItems = await new ZabbixQueryItemRequest(this.authToken, this.cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
templateids: templateids,
selectPreprocessing: "extend"
}));
if (!isZabbixErrorResult(allItems) && Array.isArray(allItems)) {
const itemidToPreprocessing = new Map<string, any>();
allItems.forEach((item: any) => {
itemidToPreprocessing.set(item.itemid, item.preprocessing);
});
for (let template of result) {
for (let item of template.items || []) {
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
}
}
}
}
}
return result;
}
} }
@ -90,7 +120,7 @@ export class TemplateHelper {
logger.error(`Unable to find templateName=${templateName}`) logger.error(`Unable to find templateName=${templateName}`)
return null return null
} }
result.push(...templates.map((t) => Number(t.templateid))) result.push(...templates.map((t: ZabbixQueryTemplateResponse) => Number(t.templateid)))
} }
return result return result
} }

View file

@ -5,7 +5,7 @@ import {TemplateImporter} from "./template_importer.js";
import {TemplateDeleter} from "./template_deleter.js"; import {TemplateDeleter} from "./template_deleter.js";
import {logger} from "../logging/logger.js"; import {logger} from "../logging/logger.js";
import {zabbixAPI} from "../datasources/zabbix-api.js"; import {zabbixAPI} from "../datasources/zabbix-api.js";
import {ZabbixQueryHostsGenericRequest} from "../datasources/zabbix-hosts.js"; import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js";
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js"; import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
import {ParsedArgs} from "../datasources/zabbix-request.js"; import {ParsedArgs} from "../datasources/zabbix-request.js";
@ -198,6 +198,83 @@ export class RegressionTestExecutor {
} }
} }
// 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) // Step 1: Create Host Group (Legacy test kept for compatibility)
const groupResult = await HostImporter.importHostGroups([{ const groupResult = await HostImporter.importHostGroups([{
groupName: groupName groupName: groupName
@ -214,9 +291,11 @@ export class RegressionTestExecutor {
// Cleanup // Cleanup
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie); await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, macroHostName, 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, regTemplateName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie); await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, macroTemplateName, 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 // We don't delete the group here as it might be shared or used by other tests in this run
} catch (error: any) { } catch (error: any) {

View file

@ -11,7 +11,8 @@ import {
ZabbixCreateTemplateGroupRequest, ZabbixCreateTemplateGroupRequest,
ZabbixCreateTemplateRequest, ZabbixCreateTemplateRequest,
ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupRequest,
ZabbixQueryTemplatesRequest ZabbixQueryTemplatesRequest,
ZabbixQueryTemplateResponse
} from "../datasources/zabbix-templates.js"; } from "../datasources/zabbix-templates.js";
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js"; import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js";
import {zabbixAPI} from "../datasources/zabbix-api.js"; import {zabbixAPI} from "../datasources/zabbix-api.js";
@ -121,7 +122,7 @@ export class TemplateImporter {
}) })
continue continue
} }
linkedTemplates = queryResult.map(t => ({ templateid: t.templateid })) linkedTemplates = queryResult.map((t: ZabbixQueryTemplateResponse) => ({ templateid: t.templateid }))
} }
// 3. Create Template // 3. Create Template

View file

@ -191,12 +191,16 @@ export interface CreateTemplateItem {
master_item?: InputMaybe<CreateMasterItem>; master_item?: InputMaybe<CreateMasterItem>;
/** Name of the item. */ /** Name of the item. */
name: Scalars['String']['input']; name: Scalars['String']['input'];
/** JavaScript code for Script items or other parameters. */
params?: InputMaybe<Scalars['String']['input']>;
/** Preprocessing steps for the item values. */ /** Preprocessing steps for the item values. */
preprocessing?: InputMaybe<Array<CreateItemPreprocessing>>; preprocessing?: InputMaybe<Array<CreateItemPreprocessing>>;
/** Zabbix item status (0 for Enabled, 1 for Disabled). */ /** Zabbix item status (0 for Enabled, 1 for Disabled). */
status?: InputMaybe<Scalars['Int']['input']>; status?: InputMaybe<Scalars['Int']['input']>;
/** Tags to assign to the item. */ /** Tags to assign to the item. */
tags?: InputMaybe<Array<CreateTag>>; tags?: InputMaybe<Array<CreateTag>>;
/** Timeout for item data collection. */
timeout?: InputMaybe<Scalars['String']['input']>;
/** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */ /** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */
type?: InputMaybe<Scalars['Int']['input']>; type?: InputMaybe<Scalars['Int']['input']>;
/** Units of the value. */ /** Units of the value. */
@ -1108,6 +1112,8 @@ export interface ZabbixItem {
delay?: Maybe<Scalars['String']['output']>; delay?: Maybe<Scalars['String']['output']>;
/** Description of the item. */ /** Description of the item. */
description?: Maybe<Scalars['String']['output']>; description?: Maybe<Scalars['String']['output']>;
/** Error message if the item is in an error state. */
error?: Maybe<Scalars['String']['output']>;
/** History storage period (e.g. '2d', '90d'). */ /** History storage period (e.g. '2d', '90d'). */
history?: Maybe<Scalars['String']['output']>; history?: Maybe<Scalars['String']['output']>;
/** Internal Zabbix ID of the host this item belongs to. */ /** Internal Zabbix ID of the host this item belongs to. */
@ -1743,6 +1749,7 @@ export type ZabbixItemResolvers<ContextType = any, ParentType extends ResolversP
attributeName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>; attributeName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
delay?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>; delay?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>; description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
error?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
history?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>; history?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
hostid?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>; hostid?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
hosts?: Resolver<Maybe<Array<ResolversTypes['Host']>>, ParentType, ContextType>; hosts?: Resolver<Maybe<Array<ResolversTypes['Host']>>, ParentType, ContextType>;