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:
parent
41e4c4da1f
commit
ad104acde2
13 changed files with 378 additions and 45 deletions
|
|
@ -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}¤t=temperature_2m,weather_code`
|
- 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.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"]) {
|
||||||
... on WeatherSensorDevice {
|
|
||||||
name
|
name
|
||||||
|
... on WeatherSensorDevice {
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -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}¤t=temperature_2m,weather_code",
|
"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."
|
"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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
46
schema/extensions/ground_value_checker.graphql
Normal file
46
schema/extensions/ground_value_checker.graphql
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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!]
|
||||||
|
|
|
||||||
|
|
@ -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!
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,8 +106,25 @@ 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;
|
||||||
|
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<string, any>();
|
||||||
|
allItems.forEach((item: any) => {
|
||||||
|
itemidToPreprocessing.set(item.itemid, item.preprocessing);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let device of hosts) {
|
||||||
for (let item of device.items || []) {
|
for (let item of device.items || []) {
|
||||||
|
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
|
||||||
if (!item.lastclock ) {
|
if (!item.lastclock ) {
|
||||||
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
||||||
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
||||||
|
|
@ -116,7 +142,8 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue