From ad104acde25358c0ef3d33d7832bc3d83fe98b3a Mon Sep 17 00:00:00 2001 From: Andreas Hilbig Date: Sun, 1 Feb 2026 21:07:21 +0100 Subject: [PATCH] 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. --- docs/howtos/cookbook.md | 97 +++++++++++++++---- ...port_ground_value_checker_template.graphql | 69 +++++++++++++ ...ple_import_weather_sensor_template.graphql | 5 +- .../extensions/ground_value_checker.graphql | 46 +++++++++ schema/extensions/weather_sensor.graphql | 4 +- schema/mutations.graphql | 8 ++ schema/zabbix.graphql | 4 + src/api/resolvers.ts | 4 + src/datasources/zabbix-hosts.ts | 59 ++++++++--- src/datasources/zabbix-templates.ts | 34 ++++++- src/execution/regression_test_executor.ts | 81 +++++++++++++++- src/execution/template_importer.ts | 5 +- src/schema/generated/graphql.ts | 7 ++ 13 files changed, 378 insertions(+), 45 deletions(-) create mode 100644 docs/queries/sample_import_ground_value_checker_template.graphql create mode 100644 schema/extensions/ground_value_checker.graphql diff --git a/docs/howtos/cookbook.md b/docs/howtos/cookbook.md index cfdc7d2..a1d6aa7 100644 --- a/docs/howtos/cookbook.md +++ b/docs/howtos/cookbook.md @@ -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 - 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 -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 type WeatherSensorDevice implements Host & Device { hostid: ID! @@ -222,34 +223,83 @@ type WeatherSensorState implements DeviceState { } type WeatherSensorValues { + """ + Current temperature at the device location (in °C). + """ temperature: Float + """ + Warnings or description of the street conditions (e.g. Ice, Rain, Clear). Derived from Open-Meteo weather codes. + """ 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 -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 -ADDITIONAL_SCHEMAS=./schema/extensions/weather_sensor.graphql -ADDITIONAL_RESOLVERS=WeatherSensorDevice +ADDITIONAL_SCHEMAS=./schema/extensions/weather_sensor.graphql,./schema/extensions/ground_value_checker.graphql +ADDITIONAL_RESOLVERS=WeatherSensorDevice,GroundValueChecker ``` Restart the API server to apply the changes. -### 🚀 Step 3: Import the Weather Sensor Template -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. +### 📊 Step 3: Analyse Data Update Frequency +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) - URL: `https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}¤t=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.streetConditionWarnings` (JavaScript mapping from `$.current.weather_code`) + - Units: `°C` + - Description: The current temperature at the device location. -### ✅ Step 4: Verification -Create a host, assign it macros for coordinates, and query its weather state. +**Key Configuration for Ground Value Checker**: +- **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 mutation CreateWeatherHost { 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: "{$LON}", value: "13.41" } ], - location: { - name: "Berlin" - } + location: { name: "Berlin" } }]) { hostid } @@ -272,10 +320,10 @@ Create a host, assign it macros for coordinates, and query its weather state. 2. **Query Data**: ```graphql - query GetWeather { - allDevices(tag_deviceType: ["WeatherSensorDevice"]) { + query GetDeviceState { + allDevices(tag_deviceType: ["WeatherSensorDevice", "GroundValueChecker"]) { + name ... on WeatherSensorDevice { - name state { current { temperature @@ -283,6 +331,13 @@ Create a host, assign it macros for coordinates, and query its weather state. } } } + ... on GroundValueChecker { + state { + current { + averageValue + } + } + } } } ``` diff --git a/docs/queries/sample_import_ground_value_checker_template.graphql b/docs/queries/sample_import_ground_value_checker_template.graphql new file mode 100644 index 0000000..880a13a --- /dev/null +++ b/docs/queries/sample_import_ground_value_checker_template.graphql @@ -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"] + } + ] + } + ] + } + ] +} +``` diff --git a/docs/queries/sample_import_weather_sensor_template.graphql b/docs/queries/sample_import_weather_sensor_template.graphql index f945295..6998cd4 100644 --- a/docs/queries/sample_import_weather_sensor_template.graphql +++ b/docs/queries/sample_import_weather_sensor_template.graphql @@ -39,7 +39,7 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's "key": "weather.get", "value_type": 4, "history": "0", - "delay": "1m", + "delay": "1h", "url": "https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}¤t=temperature_2m,weather_code", "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", "value_type": 0, "history": "7d", + "units": "°C", + "description": "The current temperature at the device location.", "master_item": { "key": "weather.get" }, @@ -65,6 +67,7 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's "key": "state.current.streetConditionWarnings", "value_type": 4, "history": "7d", + "description": "Human-readable weather warnings or descriptions derived from weather codes.", "master_item": { "key": "weather.get" }, diff --git a/schema/extensions/ground_value_checker.graphql b/schema/extensions/ground_value_checker.graphql new file mode 100644 index 0000000..05bcc3d --- /dev/null +++ b/schema/extensions/ground_value_checker.graphql @@ -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 +} diff --git a/schema/extensions/weather_sensor.graphql b/schema/extensions/weather_sensor.graphql index d9dc7dd..15c2180 100644 --- a/schema/extensions/weather_sensor.graphql +++ b/schema/extensions/weather_sensor.graphql @@ -40,11 +40,11 @@ Aggregated weather information retrieved from the API. """ type WeatherSensorValues { """ - Current temperature at the device location (in Celsius). + Current temperature at the device location (in °C). """ 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 } diff --git a/schema/mutations.graphql b/schema/mutations.graphql index 136787c..f528517 100644 --- a/schema/mutations.graphql +++ b/schema/mutations.graphql @@ -313,6 +313,14 @@ input CreateTemplateItem { """ 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: [CreateItemPreprocessing!] diff --git a/schema/zabbix.graphql b/schema/zabbix.graphql index c554353..7d110be 100644 --- a/schema/zabbix.graphql +++ b/schema/zabbix.graphql @@ -80,6 +80,10 @@ type ZabbixItem { """ 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). """ value_type: Int! diff --git a/src/api/resolvers.ts b/src/api/resolvers.ts index e5339e7..cf68f51 100644 --- a/src/api/resolvers.ts +++ b/src/api/resolvers.ts @@ -24,6 +24,7 @@ import { StorageItemType, } from "../schema/generated/graphql.js"; +import { DateTimeResolver, JSONObjectResolver, TimeResolver } from "graphql-scalars"; import {HostImporter} from "../execution/host_importer.js"; import {HostDeleter} from "../execution/host_deleter.js"; import {SmoketestExecutor} from "../execution/smoketest_executor.js"; @@ -67,6 +68,9 @@ export function createResolvers(): Resolvers { // @ts-ignore // @ts-ignore return { + DateTime: DateTimeResolver, + Time: TimeResolver, + JSONObject: JSONObjectResolver, Query: { userPermissions: async (_parent: any, objectNamesFilter: QueryUserPermissionsArgs, { zabbixAuthToken, diff --git a/src/datasources/zabbix-hosts.ts b/src/datasources/zabbix-hosts.ts index 8bc7b46..f021fa4 100644 --- a/src/datasources/zabbix-hosts.ts +++ b/src/datasources/zabbix-hosts.ts @@ -9,6 +9,7 @@ import { ZabbixResult } from "./zabbix-request.js"; import {ZabbixHistoryGetParams, ZabbixQueryHistoryRequest} from "./zabbix-history.js"; +import {ZabbixQueryItemRequest} from "./zabbix-templates.js"; export class ZabbixQueryHostsGenericRequest extends ZabbixRequest { @@ -80,6 +81,14 @@ export class ZabbixQueryHostsGenericRequestWithItemsresult) { - for (let item of device.items || []) { - 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; + const hosts = result; + const hostids = hosts.map(h => h.hostid); + + if (hostids.length > 0) { + // Batch fetch preprocessing for all items of these hosts + const allItems = await new ZabbixQueryItemRequest(this.authToken, this.cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({ + hostids: hostids, + selectPreprocessing: "extend" + })); + + if (!isZabbixErrorResult(allItems) && Array.isArray(allItems)) { + const itemidToPreprocessing = new Map(); + allItems.forEach((item: any) => { + itemidToPreprocessing.set(item.itemid, item.preprocessing); + }); + + 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; diff --git a/src/datasources/zabbix-templates.ts b/src/datasources/zabbix-templates.ts index 792d348..5c08443 100644 --- a/src/datasources/zabbix-templates.ts +++ b/src/datasources/zabbix-templates.ts @@ -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 {logger} from "../logging/logger.js"; @@ -24,6 +24,36 @@ export class ZabbixQueryTemplatesRequest extends ZabbixRequest { + 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(); + 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}`) return null } - result.push(...templates.map((t) => Number(t.templateid))) + result.push(...templates.map((t: ZabbixQueryTemplateResponse) => Number(t.templateid))) } return result } diff --git a/src/execution/regression_test_executor.ts b/src/execution/regression_test_executor.ts index d4adf96..eefd8c8 100644 --- a/src/execution/regression_test_executor.ts +++ b/src/execution/regression_test_executor.ts @@ -5,7 +5,7 @@ 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} from "../datasources/zabbix-hosts.js"; +import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js"; import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.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) const groupResult = await HostImporter.importHostGroups([{ groupName: groupName @@ -214,9 +291,11 @@ export class RegressionTestExecutor { // 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) { diff --git a/src/execution/template_importer.ts b/src/execution/template_importer.ts index 0e0040e..5a82af2 100644 --- a/src/execution/template_importer.ts +++ b/src/execution/template_importer.ts @@ -11,7 +11,8 @@ import { ZabbixCreateTemplateGroupRequest, ZabbixCreateTemplateRequest, ZabbixQueryTemplateGroupRequest, - ZabbixQueryTemplatesRequest + ZabbixQueryTemplatesRequest, + ZabbixQueryTemplateResponse } from "../datasources/zabbix-templates.js"; import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js"; import {zabbixAPI} from "../datasources/zabbix-api.js"; @@ -121,7 +122,7 @@ export class TemplateImporter { }) continue } - linkedTemplates = queryResult.map(t => ({ templateid: t.templateid })) + linkedTemplates = queryResult.map((t: ZabbixQueryTemplateResponse) => ({ templateid: t.templateid })) } // 3. Create Template diff --git a/src/schema/generated/graphql.ts b/src/schema/generated/graphql.ts index e1c3128..5179d36 100644 --- a/src/schema/generated/graphql.ts +++ b/src/schema/generated/graphql.ts @@ -191,12 +191,16 @@ export interface CreateTemplateItem { master_item?: InputMaybe; /** Name of the item. */ name: Scalars['String']['input']; + /** JavaScript code for Script items or other parameters. */ + params?: InputMaybe; /** Preprocessing steps for the item values. */ preprocessing?: InputMaybe>; /** Zabbix item status (0 for Enabled, 1 for Disabled). */ status?: InputMaybe; /** Tags to assign to the item. */ tags?: InputMaybe>; + /** Timeout for item data collection. */ + timeout?: InputMaybe; /** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */ type?: InputMaybe; /** Units of the value. */ @@ -1108,6 +1112,8 @@ export interface ZabbixItem { delay?: Maybe; /** Description of the item. */ description?: Maybe; + /** Error message if the item is in an error state. */ + error?: Maybe; /** History storage period (e.g. '2d', '90d'). */ history?: Maybe; /** Internal Zabbix ID of the host this item belongs to. */ @@ -1743,6 +1749,7 @@ export type ZabbixItemResolvers, ParentType, ContextType>; delay?: Resolver, ParentType, ContextType>; description?: Resolver, ParentType, ContextType>; + error?: Resolver, ParentType, ContextType>; history?: Resolver, ParentType, ContextType>; hostid?: Resolver, ParentType, ContextType>; hosts?: Resolver>, ParentType, ContextType>;