feat: add weather sensor support and fix device status mapping
This commit introduces support for provisioning weather sensors with geo-coordinates
via user macros and fixes a critical mapping bug in device status.
Changes:
- fix: Corrected DeviceStatus enum mapping (0=ENABLED, 1=DISABLED).
- feat: Added 'status' field to CreateTemplateItem input in GraphQL schema.
- feat: Enabled user macro assignment during host and template creation/import.
- feat: Added regression tests for user macro assignment and HTTP agent URL support.
- docs: Updated cookbook and sample queries to use {$LAT} and {$LON} macros.
- test: Added unit tests for macro assignment in HostImporter and TemplateImporter.
- chore: Regenerated GraphQL types.
This commit is contained in:
parent
5da4a17e36
commit
41e4c4da1f
12 changed files with 251 additions and 60 deletions
99
.idea/workspace.xml
generated
99
.idea/workspace.xml
generated
|
|
@ -6,11 +6,17 @@
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24 This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP). Changes: - Environment & CI/CD: - Set Node.js version to >=24 in package.json and .nvmrc. - Updated Dockerfile to use Node 24 base image. - Updated @types/node to ^24.10.9. - Documentation: - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping. - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation. - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol. - Added readme.improvement.plan.md to track documentation evolution. - Enhanced all how-to guides with improved cross-references and up-to-date information. - Guidelines: - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0). - Infrastructure & Code: - Updated docker-compose.yml with Apollo MCP server integration. - Refined configuration and schema handling in src/api/ and src/datasources/. - Synchronized generated TypeScript types with schema updates.">
|
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24 This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP). Changes: - Environment & CI/CD: - Set Node.js version to >=24 in package.json and .nvmrc. - Updated Dockerfile to use Node 24 base image. - Updated @types/node to ^24.10.9. - Documentation: - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping. - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation. - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol. - Added readme.improvement.plan.md to track documentation evolution. - Enhanced all how-to guides with improved cross-references and up-to-date information. - Guidelines: - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0). - Infrastructure & Code: - Updated docker-compose.yml with Apollo MCP server integration. - Refined configuration and schema handling in src/api/ and src/datasources/. - Synchronized generated TypeScript types with schema updates.">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/howtos/cookbook.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/cookbook.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/queries/sample_import_weather_sensor_template.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/docs/queries/sample_import_weather_sensor_template.graphql" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/schema/mutations.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/schema/mutations.graphql" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/schema/mutations.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/schema/mutations.graphql" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/api/resolvers.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/resolvers.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-hosts.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hosts.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-templates.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-templates.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/execution/host_importer.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/host_importer.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/execution/host_importer.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/host_importer.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/execution/template_importer.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/template_importer.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/model/model_enum_values.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/model/model_enum_values.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/schema/generated/graphql.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/schema/generated/graphql.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/schema/generated/graphql.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/schema/generated/graphql.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/host_importer.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/host_importer.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/template_importer.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_importer.test.ts" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
@ -21,7 +27,7 @@
|
||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="138" />
|
<option name="cachedIndexableFilesCount" value="149" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
|
|
@ -69,51 +75,51 @@
|
||||||
<option name="openDirectoriesWithSingleClick" value="true" />
|
<option name="openDirectoriesWithSingleClick" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
"NIXITCH_NIXPKGS_CONFIG": "",
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
"NIXITCH_NIX_CONF_DIR": "",
|
||||||
"NIXITCH_NIX_OTHER_STORES": "",
|
"NIXITCH_NIX_OTHER_STORES": "",
|
||||||
"NIXITCH_NIX_PATH": "",
|
"NIXITCH_NIX_PATH": "",
|
||||||
"NIXITCH_NIX_PROFILES": "",
|
"NIXITCH_NIX_PROFILES": "",
|
||||||
"NIXITCH_NIX_REMOTE": "",
|
"NIXITCH_NIX_REMOTE": "",
|
||||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
||||||
"Node.js.index.ts.executor": "Run",
|
"Node.js.index.ts.executor": "Run",
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "main",
|
||||||
"go.import.settings.migrated": "true",
|
"go.import.settings.migrated": "true",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
"junie.onboarding.icon.badge.shown": "true",
|
||||||
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/src",
|
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/src",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
|
"nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"npm.codegen.executor": "Run",
|
"npm.codegen.executor": "Run",
|
||||||
"npm.compile.executor": "Run",
|
"npm.compile.executor": "Run",
|
||||||
"npm.copy-schema.executor": "Run",
|
"npm.copy-schema.executor": "Run",
|
||||||
"npm.prod.executor": "Run",
|
"npm.prod.executor": "Run",
|
||||||
"npm.test.executor": "Run",
|
"npm.test.executor": "Run",
|
||||||
"settings.editor.selected.configurable": "ml.llm.mcp",
|
"settings.editor.selected.configurable": "ml.llm.mcp",
|
||||||
"settings.editor.splitter.proportion": "0.28812414",
|
"settings.editor.splitter.proportion": "0.28812414",
|
||||||
"to.speed.mode.migration.done": "true",
|
"to.speed.mode.migration.done": "true",
|
||||||
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RecapSpentCounter">
|
<component name="RecapSpentCounter">
|
||||||
<option name="endsOfQuotaMs" value="1768327208764" />
|
<option name="endsOfQuotaMs" value="1772398800000" />
|
||||||
<option name="spentUsd" value="0.04010335" />
|
<option name="spentUsd" value="0.01011225" />
|
||||||
</component>
|
</component>
|
||||||
<component name="RecapUselessUpdatesCounter">
|
<component name="RecapUselessUpdatesCounter">
|
||||||
<option name="suspendCountdown" value="0" />
|
<option name="suspendCountdown" value="8" />
|
||||||
</component>
|
</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
|
@ -211,7 +217,8 @@
|
||||||
<workItem from="1769700092648" duration="5212000" />
|
<workItem from="1769700092648" duration="5212000" />
|
||||||
<workItem from="1769724930397" duration="16056000" />
|
<workItem from="1769724930397" duration="16056000" />
|
||||||
<workItem from="1769789496322" duration="14281000" />
|
<workItem from="1769789496322" duration="14281000" />
|
||||||
<workItem from="1769849767328" duration="4117000" />
|
<workItem from="1769849767328" duration="18404000" />
|
||||||
|
<workItem from="1769955114366" duration="3276000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment">
|
<task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|
@ -476,7 +483,7 @@
|
||||||
<breakpoints>
|
<breakpoints>
|
||||||
<line-breakpoint enabled="true" type="javascript">
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
|
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
|
||||||
<line>133</line>
|
<line>134</line>
|
||||||
<option name="timeStamp" value="5" />
|
<option name="timeStamp" value="5" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
<line-breakpoint enabled="true" type="javascript">
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ This recipe demonstrates how to extend the schema with a new device type that re
|
||||||
|
|
||||||
### 📋 Prerequisites
|
### 📋 Prerequisites
|
||||||
- Zabbix GraphQL API is running.
|
- Zabbix GraphQL API is running.
|
||||||
- The device has geo-coordinates set in its inventory (`location_lat` and `location_lon`).
|
- The device has geo-coordinates set via user macros (`{$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/` named `weather_sensor.graphql`.
|
||||||
|
|
@ -242,12 +242,12 @@ Use the `importTemplates` mutation to create the `WEATHER_SENSOR` template. This
|
||||||
|
|
||||||
**Key Item Configuration**:
|
**Key Item Configuration**:
|
||||||
- **Master Item**: `weather.get` (HTTP Agent)
|
- **Master Item**: `weather.get` (HTTP Agent)
|
||||||
- URL: `https://api.open-meteo.com/v1/forecast?latitude={INVENTORY.LOCATION.LAT}&longitude={INVENTORY.LOCATION.LON}¤t=temperature_2m,weather_code`
|
- URL: `https://api.open-meteo.com/v1/forecast?latitude={$LAT}&longitude={$LON}¤t=temperature_2m,weather_code`
|
||||||
- **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`)
|
- **Dependent Item**: `state.current.streetConditionWarnings` (JavaScript mapping from `$.current.weather_code`)
|
||||||
|
|
||||||
### ✅ Step 4: Verification
|
### ✅ Step 4: Verification
|
||||||
Create a host, assign it coordinates, and query its weather state.
|
Create a host, assign it macros for coordinates, and query its weather state.
|
||||||
|
|
||||||
1. **Create Host**:
|
1. **Create Host**:
|
||||||
```graphql
|
```graphql
|
||||||
|
|
@ -257,10 +257,12 @@ Create a host, assign it coordinates, and query its weather state.
|
||||||
deviceType: "WeatherSensorDevice",
|
deviceType: "WeatherSensorDevice",
|
||||||
groupNames: ["External Sensors"],
|
groupNames: ["External Sensors"],
|
||||||
templateNames: ["WEATHER_SENSOR"],
|
templateNames: ["WEATHER_SENSOR"],
|
||||||
|
macros: [
|
||||||
|
{ macro: "{$LAT}", value: "52.52" },
|
||||||
|
{ macro: "{$LON}", value: "13.41" }
|
||||||
|
],
|
||||||
location: {
|
location: {
|
||||||
name: "Berlin",
|
name: "Berlin"
|
||||||
location_lat: "52.52",
|
|
||||||
location_lon: "13.41"
|
|
||||||
}
|
}
|
||||||
}]) {
|
}]) {
|
||||||
hostid
|
hostid
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ mutation ImportWeatherSensorTemplate($templates: [CreateTemplate!]!) {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
The following variables define the `WEATHER_SENSOR` template. It uses the host's inventory coordinates (`{INVENTORY.LOCATION_LAT}` and `{INVENTORY.LOCATION_LON}`) to fetch localized weather data.
|
The following variables define the `WEATHER_SENSOR` template. It uses the host's user macros (`{$LAT}` and `{$LON}`) to fetch localized weather data.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +28,10 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
|
||||||
"tags": [
|
"tags": [
|
||||||
{ "tag": "deviceType", "value": "WeatherSensorDevice" }
|
{ "tag": "deviceType", "value": "WeatherSensorDevice" }
|
||||||
],
|
],
|
||||||
|
"macros": [
|
||||||
|
{ "macro": "{$LAT}", "value": "52.52" },
|
||||||
|
{ "macro": "{$LON}", "value": "13.41" }
|
||||||
|
],
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "Open-Meteo API Fetch",
|
"name": "Open-Meteo API Fetch",
|
||||||
|
|
@ -36,7 +40,7 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
|
||||||
"value_type": 4,
|
"value_type": 4,
|
||||||
"history": "0",
|
"history": "0",
|
||||||
"delay": "1m",
|
"delay": "1m",
|
||||||
"url": "https://api.open-meteo.com/v1/forecast?latitude={INVENTORY.LOCATION.LAT}&longitude={INVENTORY.LOCATION.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."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,10 @@ input CreateTemplate {
|
||||||
Tags to assign to the template.
|
Tags to assign to the template.
|
||||||
"""
|
"""
|
||||||
tags: [CreateTag!]
|
tags: [CreateTag!]
|
||||||
|
"""
|
||||||
|
User macros to assign to the template.
|
||||||
|
"""
|
||||||
|
macros: [CreateMacro!]
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -277,6 +281,10 @@ input CreateTemplateItem {
|
||||||
"""
|
"""
|
||||||
type: Int
|
type: Int
|
||||||
"""
|
"""
|
||||||
|
Zabbix item status (0 for Enabled, 1 for Disabled).
|
||||||
|
"""
|
||||||
|
status: Int
|
||||||
|
"""
|
||||||
Technical key of the item.
|
Technical key of the item.
|
||||||
"""
|
"""
|
||||||
key: String!
|
key: String!
|
||||||
|
|
@ -374,6 +382,20 @@ input CreateTag {
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for creating a user macro.
|
||||||
|
"""
|
||||||
|
input CreateMacro {
|
||||||
|
"""
|
||||||
|
Macro name (e.g. '{$LAT}').
|
||||||
|
"""
|
||||||
|
macro: String!
|
||||||
|
"""
|
||||||
|
Macro value.
|
||||||
|
"""
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Response for a template import operation.
|
Response for a template import operation.
|
||||||
"""
|
"""
|
||||||
|
|
@ -512,6 +534,10 @@ input CreateHost {
|
||||||
Location information for the host.
|
Location information for the host.
|
||||||
"""
|
"""
|
||||||
location: LocationInput
|
location: LocationInput
|
||||||
|
"""
|
||||||
|
User macros to assign to the host.
|
||||||
|
"""
|
||||||
|
macros: [CreateMacro!]
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,8 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
|
||||||
}
|
}
|
||||||
templateids?: [number];
|
templateids?: [number];
|
||||||
hostgroupids?: [number];
|
hostgroupids?: [number];
|
||||||
additionalParams?: [number];
|
macros?: { macro: string, value: string }[];
|
||||||
|
additionalParams?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,6 +201,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
||||||
return {groupid: groupid}
|
return {groupid: groupid}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (inputParams.macros) {
|
||||||
|
this.macros = inputParams.macros;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
host: string
|
host: string
|
||||||
|
|
@ -214,6 +218,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
||||||
}
|
}
|
||||||
templates?: any
|
templates?: any
|
||||||
groups?: any
|
groups?: any
|
||||||
|
macros?: { macro: string, value: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ export class HostImporter {
|
||||||
name: device.name,
|
name: device.name,
|
||||||
location: device.location,
|
location: device.location,
|
||||||
templateids: templateids,
|
templateids: templateids,
|
||||||
hostgroupids: groupids
|
hostgroupids: groupids,
|
||||||
|
macros: device.macros
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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} from "../datasources/zabbix-hosts.js";
|
||||||
|
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
||||||
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
||||||
|
|
||||||
export class RegressionTestExecutor {
|
export class RegressionTestExecutor {
|
||||||
|
|
@ -78,7 +79,71 @@ export class RegressionTestExecutor {
|
||||||
});
|
});
|
||||||
if (!httpSuccess) success = false;
|
if (!httpSuccess) success = false;
|
||||||
|
|
||||||
// Regression 4: Host retrieval and visibility (allHosts output fields fix)
|
// Regression 4: User Macro assignment for host and template creation
|
||||||
|
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
const macroTempResult = await TemplateImporter.importTemplates([{
|
||||||
|
host: macroTemplateName,
|
||||||
|
name: "Regression Macro Template",
|
||||||
|
groupNames: [regGroupName],
|
||||||
|
macros: [
|
||||||
|
{ macro: "{$TEMP_MACRO}", value: "temp_value" }
|
||||||
|
]
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
const macroTempImportSuccess = !!macroTempResult?.length && !macroTempResult[0].error;
|
||||||
|
let macroHostImportSuccess = false;
|
||||||
|
let macroVerifySuccess = false;
|
||||||
|
|
||||||
|
if (macroTempImportSuccess) {
|
||||||
|
const macroHostResult = await HostImporter.importHosts([{
|
||||||
|
deviceKey: macroHostName,
|
||||||
|
deviceType: "RegressionHost",
|
||||||
|
groupNames: [hostGroupName],
|
||||||
|
templateNames: [macroTemplateName],
|
||||||
|
macros: [
|
||||||
|
{ macro: "{$HOST_MACRO}", value: "host_value" }
|
||||||
|
]
|
||||||
|
}], zabbixAuthToken, cookie);
|
||||||
|
macroHostImportSuccess = !!macroHostResult?.length && !!macroHostResult[0].hostid;
|
||||||
|
|
||||||
|
if (macroHostImportSuccess) {
|
||||||
|
// Verify macros on host
|
||||||
|
const verifyHostResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
|
||||||
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||||
|
filter_host: macroHostName,
|
||||||
|
selectMacros: "extend"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify macros on template
|
||||||
|
const verifyTempResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||||
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||||
|
filter_host: macroTemplateName,
|
||||||
|
selectMacros: "extend"
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hasHostMacro = Array.isArray(verifyHostResult) && verifyHostResult.length > 0 &&
|
||||||
|
(verifyHostResult[0] as any).macros?.some((m: any) => m.macro === "{$HOST_MACRO}" && m.value === "host_value");
|
||||||
|
|
||||||
|
const hasTempMacro = Array.isArray(verifyTempResult) && verifyTempResult.length > 0 &&
|
||||||
|
(verifyTempResult[0] as any).macros?.some((m: any) => m.macro === "{$TEMP_MACRO}" && m.value === "temp_value");
|
||||||
|
|
||||||
|
macroVerifySuccess = !!(hasHostMacro && hasTempMacro);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const macroOverallSuccess = macroTempImportSuccess && macroHostImportSuccess && macroVerifySuccess;
|
||||||
|
steps.push({
|
||||||
|
name: "REG-MACRO: User Macro assignment",
|
||||||
|
success: macroOverallSuccess,
|
||||||
|
message: macroOverallSuccess
|
||||||
|
? "Macros successfully assigned to template and host"
|
||||||
|
: `Failed: TempImport=${macroTempImportSuccess}, HostImport=${macroHostImportSuccess}, Verify=${macroVerifySuccess}`
|
||||||
|
});
|
||||||
|
if (!macroOverallSuccess) success = false;
|
||||||
|
|
||||||
|
// Regression 5: Host retrieval and visibility (allHosts output fields fix)
|
||||||
if (success) {
|
if (success) {
|
||||||
const hostResult = await HostImporter.importHosts([{
|
const hostResult = await HostImporter.importHosts([{
|
||||||
deviceKey: hostName,
|
deviceKey: hostName,
|
||||||
|
|
@ -148,8 +213,10 @@ 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 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);
|
||||||
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,8 @@ export class TemplateImporter {
|
||||||
groups: groupids.map(id => ({ groupid: id })),
|
groups: groupids.map(id => ({ groupid: id })),
|
||||||
uuid: template.uuid,
|
uuid: template.uuid,
|
||||||
templates: linkedTemplates,
|
templates: linkedTemplates,
|
||||||
tags: template.tags?.map(t => ({ tag: t.tag, value: t.value || "" }))
|
tags: template.tags?.map(t => ({ tag: t.tag, value: t.value || "" })),
|
||||||
|
macros: template.macros
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateImportResult = await new ZabbixCreateTemplateRequest(zabbixAuthToken, cookie)
|
let templateImportResult = await new ZabbixCreateTemplateRequest(zabbixAuthToken, cookie)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export enum DeviceCommunicationType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DeviceStatus {
|
export enum DeviceStatus {
|
||||||
DISABLED = "0",
|
ENABLED = "0",
|
||||||
ENABLED = "1"
|
DISABLED = "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StorageItemType {
|
export enum StorageItemType {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ export interface CreateHost {
|
||||||
groupids?: InputMaybe<Array<InputMaybe<Scalars['Int']['input']>>>;
|
groupids?: InputMaybe<Array<InputMaybe<Scalars['Int']['input']>>>;
|
||||||
/** Location information for the host. */
|
/** Location information for the host. */
|
||||||
location?: InputMaybe<LocationInput>;
|
location?: InputMaybe<LocationInput>;
|
||||||
|
/** User macros to assign to the host. */
|
||||||
|
macros?: InputMaybe<Array<CreateMacro>>;
|
||||||
/** Optional display name of the device (must be unique if provided - default is to set display name to deviceKey). */
|
/** Optional display name of the device (must be unique if provided - default is to set display name to deviceKey). */
|
||||||
name?: InputMaybe<Scalars['String']['input']>;
|
name?: InputMaybe<Scalars['String']['input']>;
|
||||||
/** List of template names to link to the host. */
|
/** List of template names to link to the host. */
|
||||||
|
|
@ -110,6 +112,14 @@ export interface CreateLinkedTemplate {
|
||||||
name: Scalars['String']['input'];
|
name: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Input for creating a user macro. */
|
||||||
|
export interface CreateMacro {
|
||||||
|
/** Macro name (e.g. '{$LAT}'). */
|
||||||
|
macro: Scalars['String']['input'];
|
||||||
|
/** Macro value. */
|
||||||
|
value: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
/** Reference to a master item for dependent items. */
|
/** Reference to a master item for dependent items. */
|
||||||
export interface CreateMasterItem {
|
export interface CreateMasterItem {
|
||||||
/** The technical key of the master item. */
|
/** The technical key of the master item. */
|
||||||
|
|
@ -134,6 +144,8 @@ export interface CreateTemplate {
|
||||||
host: Scalars['String']['input'];
|
host: Scalars['String']['input'];
|
||||||
/** List of items to create within the template. */
|
/** List of items to create within the template. */
|
||||||
items?: InputMaybe<Array<CreateTemplateItem>>;
|
items?: InputMaybe<Array<CreateTemplateItem>>;
|
||||||
|
/** User macros to assign to the template. */
|
||||||
|
macros?: InputMaybe<Array<CreateMacro>>;
|
||||||
/** Visible name of the template. */
|
/** Visible name of the template. */
|
||||||
name?: InputMaybe<Scalars['String']['input']>;
|
name?: InputMaybe<Scalars['String']['input']>;
|
||||||
/** Tags to assign to the template. */
|
/** Tags to assign to the template. */
|
||||||
|
|
@ -181,6 +193,8 @@ export interface CreateTemplateItem {
|
||||||
name: Scalars['String']['input'];
|
name: 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). */
|
||||||
|
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>>;
|
||||||
/** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */
|
/** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */
|
||||||
|
|
@ -1221,6 +1235,7 @@ export type ResolversTypes = {
|
||||||
CreateHostResponse: ResolverTypeWrapper<CreateHostResponse>;
|
CreateHostResponse: ResolverTypeWrapper<CreateHostResponse>;
|
||||||
CreateItemPreprocessing: CreateItemPreprocessing;
|
CreateItemPreprocessing: CreateItemPreprocessing;
|
||||||
CreateLinkedTemplate: CreateLinkedTemplate;
|
CreateLinkedTemplate: CreateLinkedTemplate;
|
||||||
|
CreateMacro: CreateMacro;
|
||||||
CreateMasterItem: CreateMasterItem;
|
CreateMasterItem: CreateMasterItem;
|
||||||
CreateTag: CreateTag;
|
CreateTag: CreateTag;
|
||||||
CreateTemplate: CreateTemplate;
|
CreateTemplate: CreateTemplate;
|
||||||
|
|
@ -1298,6 +1313,7 @@ export type ResolversParentTypes = {
|
||||||
CreateHostResponse: CreateHostResponse;
|
CreateHostResponse: CreateHostResponse;
|
||||||
CreateItemPreprocessing: CreateItemPreprocessing;
|
CreateItemPreprocessing: CreateItemPreprocessing;
|
||||||
CreateLinkedTemplate: CreateLinkedTemplate;
|
CreateLinkedTemplate: CreateLinkedTemplate;
|
||||||
|
CreateMacro: CreateMacro;
|
||||||
CreateMasterItem: CreateMasterItem;
|
CreateMasterItem: CreateMasterItem;
|
||||||
CreateTag: CreateTag;
|
CreateTag: CreateTag;
|
||||||
CreateTemplate: CreateTemplate;
|
CreateTemplate: CreateTemplate;
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,38 @@ describe("HostImporter", () => {
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result![0].hostid).toBe("401");
|
expect(result![0].hostid).toBe("401");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("importHosts - with macros", async () => {
|
||||||
|
const hosts = [{
|
||||||
|
deviceKey: "DeviceMacro",
|
||||||
|
deviceType: "Type1",
|
||||||
|
groupNames: ["Group1"],
|
||||||
|
macros: [
|
||||||
|
{ macro: "{$LAT}", value: "52.52" },
|
||||||
|
{ macro: "{$LON}", value: "13.41" }
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Mocking group lookup
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: ZABBIX_EDGE_DEVICE_BASE_GROUP }]);
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "202", name: ZABBIX_EDGE_DEVICE_BASE_GROUP + "/Group1" }]);
|
||||||
|
|
||||||
|
// Mocking template lookup
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "301" }]);
|
||||||
|
|
||||||
|
// Mocking host.create
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ hostids: ["402"] });
|
||||||
|
|
||||||
|
const result = await HostImporter.importHosts(hosts, "token");
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result![0].hostid).toBe("402");
|
||||||
|
|
||||||
|
// Verify that host.create was called with macros
|
||||||
|
const hostCreateCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "host.create");
|
||||||
|
expect(hostCreateCall[1].body.params.macros).toEqual([
|
||||||
|
{ macro: "{$LAT}", value: "52.52" },
|
||||||
|
{ macro: "{$LON}", value: "13.41" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -163,4 +163,32 @@ describe("TemplateImporter", () => {
|
||||||
expect(result![0].message).toContain("Invalid params.");
|
expect(result![0].message).toContain("Invalid params.");
|
||||||
expect(result![0].message).toContain("the parameter \"key_\" is missing.");
|
expect(result![0].message).toContain("the parameter \"key_\" is missing.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("importTemplates - with macros", async () => {
|
||||||
|
const templates = [{
|
||||||
|
host: "TemplateWithMacros",
|
||||||
|
groupNames: ["Group1"],
|
||||||
|
macros: [
|
||||||
|
{ macro: "{$LAT}", value: "52.52" },
|
||||||
|
{ macro: "{$LON}", value: "13.41" }
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Mocking group.get
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Group1" }]);
|
||||||
|
// Mocking template.create
|
||||||
|
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["302"] });
|
||||||
|
|
||||||
|
const result = await TemplateImporter.importTemplates(templates, "token");
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result![0].templateid).toBe("302");
|
||||||
|
|
||||||
|
// Verify that template.create was called with macros
|
||||||
|
const templateCreateCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "template.create");
|
||||||
|
expect(templateCreateCall[1].body.params.macros).toEqual([
|
||||||
|
{ macro: "{$LAT}", value: "52.52" },
|
||||||
|
{ macro: "{$LON}", value: "13.41" }
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue