diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 4fc0c1d..40ef872 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -6,11 +6,17 @@
+
+
-
-
+
+
+
+
+
+
@@ -21,7 +27,7 @@
-
+
@@ -69,51 +75,51 @@
- {
- "keyToString": {
- "NIXITCH_NIXPKGS_CONFIG": "",
- "NIXITCH_NIX_CONF_DIR": "",
- "NIXITCH_NIX_OTHER_STORES": "",
- "NIXITCH_NIX_PATH": "",
- "NIXITCH_NIX_PROFILES": "",
- "NIXITCH_NIX_REMOTE": "",
- "NIXITCH_NIX_USER_PROFILE_DIR": "",
- "Node.js.index.ts.executor": "Run",
- "RunOnceActivity.MCP Project settings loaded": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
- "RunOnceActivity.git.unshallow": "true",
- "RunOnceActivity.typescript.service.memoryLimit.init": "true",
- "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
- "git-widget-placeholder": "main",
- "go.import.settings.migrated": "true",
- "javascript.preferred.runtime.type.id": "node",
- "junie.onboarding.icon.badge.shown": "true",
- "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.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
- "nodejs_package_manager_path": "npm",
- "npm.codegen.executor": "Run",
- "npm.compile.executor": "Run",
- "npm.copy-schema.executor": "Run",
- "npm.prod.executor": "Run",
- "npm.test.executor": "Run",
- "settings.editor.selected.configurable": "ml.llm.mcp",
- "settings.editor.splitter.proportion": "0.28812414",
- "to.speed.mode.migration.done": "true",
- "ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
-
-
+
+
-
+
@@ -211,7 +217,8 @@
-
+
+
@@ -476,7 +483,7 @@
file://$PROJECT_DIR$/src/datasources/zabbix-request.ts
- 133
+ 134
diff --git a/docs/howtos/cookbook.md b/docs/howtos/cookbook.md
index da802d9..cfdc7d2 100644
--- a/docs/howtos/cookbook.md
+++ b/docs/howtos/cookbook.md
@@ -200,7 +200,7 @@ This recipe demonstrates how to extend the schema with a new device type that re
### 📋 Prerequisites
- 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
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**:
- **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.streetConditionWarnings` (JavaScript mapping from `$.current.weather_code`)
### ✅ 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**:
```graphql
@@ -257,10 +257,12 @@ Create a host, assign it coordinates, and query its weather state.
deviceType: "WeatherSensorDevice",
groupNames: ["External Sensors"],
templateNames: ["WEATHER_SENSOR"],
+ macros: [
+ { macro: "{$LAT}", value: "52.52" },
+ { macro: "{$LON}", value: "13.41" }
+ ],
location: {
- name: "Berlin",
- location_lat: "52.52",
- location_lon: "13.41"
+ name: "Berlin"
}
}]) {
hostid
diff --git a/docs/queries/sample_import_weather_sensor_template.graphql b/docs/queries/sample_import_weather_sensor_template.graphql
index 4e67b21..f945295 100644
--- a/docs/queries/sample_import_weather_sensor_template.graphql
+++ b/docs/queries/sample_import_weather_sensor_template.graphql
@@ -16,7 +16,7 @@ mutation ImportWeatherSensorTemplate($templates: [CreateTemplate!]!) {
```
### 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
{
@@ -28,6 +28,10 @@ The following variables define the `WEATHER_SENSOR` template. It uses the host's
"tags": [
{ "tag": "deviceType", "value": "WeatherSensorDevice" }
],
+ "macros": [
+ { "macro": "{$LAT}", "value": "52.52" },
+ { "macro": "{$LON}", "value": "13.41" }
+ ],
"items": [
{
"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,
"history": "0",
"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."
},
{
diff --git a/schema/mutations.graphql b/schema/mutations.graphql
index 7bd633d..136787c 100644
--- a/schema/mutations.graphql
+++ b/schema/mutations.graphql
@@ -258,6 +258,10 @@ input CreateTemplate {
Tags to assign to the template.
"""
tags: [CreateTag!]
+ """
+ User macros to assign to the template.
+ """
+ macros: [CreateMacro!]
}
"""
@@ -277,6 +281,10 @@ input CreateTemplateItem {
"""
type: Int
"""
+ Zabbix item status (0 for Enabled, 1 for Disabled).
+ """
+ status: Int
+ """
Technical key of the item.
"""
key: String!
@@ -374,6 +382,20 @@ input CreateTag {
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.
"""
@@ -512,6 +534,10 @@ input CreateHost {
Location information for the host.
"""
location: LocationInput
+ """
+ User macros to assign to the host.
+ """
+ macros: [CreateMacro!]
}
"""
diff --git a/src/datasources/zabbix-hosts.ts b/src/datasources/zabbix-hosts.ts
index a8ad53b..8bc7b46 100644
--- a/src/datasources/zabbix-hosts.ts
+++ b/src/datasources/zabbix-hosts.ts
@@ -173,7 +173,8 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
}
templateids?: [number];
hostgroupids?: [number];
- additionalParams?: [number];
+ macros?: { macro: string, value: string }[];
+ additionalParams?: any;
}
@@ -200,6 +201,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
return {groupid: groupid}
});
}
+ if (inputParams.macros) {
+ this.macros = inputParams.macros;
+ }
}
host: string
@@ -214,6 +218,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
}
templates?: any
groups?: any
+ macros?: { macro: string, value: string }[]
}
diff --git a/src/execution/host_importer.ts b/src/execution/host_importer.ts
index b156efa..8198544 100644
--- a/src/execution/host_importer.ts
+++ b/src/execution/host_importer.ts
@@ -143,7 +143,8 @@ export class HostImporter {
name: device.name,
location: device.location,
templateids: templateids,
- hostgroupids: groupids
+ hostgroupids: groupids,
+ macros: device.macros
}
))
diff --git a/src/execution/regression_test_executor.ts b/src/execution/regression_test_executor.ts
index 3ae5e57..d4adf96 100644
--- a/src/execution/regression_test_executor.ts
+++ b/src/execution/regression_test_executor.ts
@@ -6,6 +6,7 @@ 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 {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
import {ParsedArgs} from "../datasources/zabbix-request.js";
export class RegressionTestExecutor {
@@ -78,7 +79,71 @@ export class RegressionTestExecutor {
});
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) {
const hostResult = await HostImporter.importHosts([{
deviceKey: hostName,
@@ -148,8 +213,10 @@ export class RegressionTestExecutor {
// Cleanup
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, 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
} catch (error: any) {
diff --git a/src/execution/template_importer.ts b/src/execution/template_importer.ts
index 83c96e5..0e0040e 100644
--- a/src/execution/template_importer.ts
+++ b/src/execution/template_importer.ts
@@ -131,7 +131,8 @@ export class TemplateImporter {
groups: groupids.map(id => ({ groupid: id })),
uuid: template.uuid,
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)
diff --git a/src/model/model_enum_values.ts b/src/model/model_enum_values.ts
index 0950bf8..30c743c 100644
--- a/src/model/model_enum_values.ts
+++ b/src/model/model_enum_values.ts
@@ -17,8 +17,8 @@ export enum DeviceCommunicationType {
}
export enum DeviceStatus {
- DISABLED = "0",
- ENABLED = "1"
+ ENABLED = "0",
+ DISABLED = "1"
}
export enum StorageItemType {
diff --git a/src/schema/generated/graphql.ts b/src/schema/generated/graphql.ts
index 87506ce..e1c3128 100644
--- a/src/schema/generated/graphql.ts
+++ b/src/schema/generated/graphql.ts
@@ -52,6 +52,8 @@ export interface CreateHost {
groupids?: InputMaybe>>;
/** Location information for the host. */
location?: InputMaybe;
+ /** User macros to assign to the host. */
+ macros?: InputMaybe>;
/** Optional display name of the device (must be unique if provided - default is to set display name to deviceKey). */
name?: InputMaybe;
/** List of template names to link to the host. */
@@ -110,6 +112,14 @@ export interface CreateLinkedTemplate {
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. */
export interface CreateMasterItem {
/** The technical key of the master item. */
@@ -134,6 +144,8 @@ export interface CreateTemplate {
host: Scalars['String']['input'];
/** List of items to create within the template. */
items?: InputMaybe>;
+ /** User macros to assign to the template. */
+ macros?: InputMaybe>;
/** Visible name of the template. */
name?: InputMaybe;
/** Tags to assign to the template. */
@@ -181,6 +193,8 @@ export interface CreateTemplateItem {
name: Scalars['String']['input'];
/** 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>;
/** Zabbix item type (e.g. 0 for Zabbix Agent, 18 for Dependent). */
@@ -1221,6 +1235,7 @@ export type ResolversTypes = {
CreateHostResponse: ResolverTypeWrapper;
CreateItemPreprocessing: CreateItemPreprocessing;
CreateLinkedTemplate: CreateLinkedTemplate;
+ CreateMacro: CreateMacro;
CreateMasterItem: CreateMasterItem;
CreateTag: CreateTag;
CreateTemplate: CreateTemplate;
@@ -1298,6 +1313,7 @@ export type ResolversParentTypes = {
CreateHostResponse: CreateHostResponse;
CreateItemPreprocessing: CreateItemPreprocessing;
CreateLinkedTemplate: CreateLinkedTemplate;
+ CreateMacro: CreateMacro;
CreateMasterItem: CreateMasterItem;
CreateTag: CreateTag;
CreateTemplate: CreateTemplate;
diff --git a/src/test/host_importer.test.ts b/src/test/host_importer.test.ts
index d36dc39..14fb94f 100644
--- a/src/test/host_importer.test.ts
+++ b/src/test/host_importer.test.ts
@@ -79,4 +79,38 @@ describe("HostImporter", () => {
expect(result).toHaveLength(1);
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" }
+ ]);
+ });
});
diff --git a/src/test/template_importer.test.ts b/src/test/template_importer.test.ts
index 8bfa18e..6258c0e 100644
--- a/src/test/template_importer.test.ts
+++ b/src/test/template_importer.test.ts
@@ -163,4 +163,32 @@ describe("TemplateImporter", () => {
expect(result![0].message).toContain("Invalid params.");
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" }
+ ]);
+ });
});