diff --git a/.idea/runConfigurations/index_ts.xml b/.idea/runConfigurations/index_ts.xml
index 052e544..1c32029 100644
--- a/.idea/runConfigurations/index_ts.xml
+++ b/.idea/runConfigurations/index_ts.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index c60a6b6..9d62e65 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,13 +4,16 @@
-
+
+
+
-
+
+
-
+
@@ -22,7 +25,7 @@
-
+
@@ -52,7 +55,7 @@
"NIXITCH_NIX_PROFILES": "",
"NIXITCH_NIX_REMOTE": "",
"NIXITCH_NIX_USER_PROFILE_DIR": "",
- "Node.js.index.ts.executor": "Debug",
+ "Node.js.index.ts.executor": "Run",
"RunOnceActivity.MCP Project settings loaded": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
@@ -63,7 +66,7 @@
"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/test",
+ "last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/src/testdata/templates",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
@@ -74,7 +77,7 @@
"npm.compile.executor": "Run",
"npm.copy-schema.executor": "Run",
"npm.prod.executor": "Run",
- "settings.editor.selected.configurable": "preferences.sourceCode.TypeScript",
+ "settings.editor.selected.configurable": "settings.javascript.runtime",
"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"
@@ -89,6 +92,7 @@
+
@@ -160,7 +164,9 @@
-
+
+
+
@@ -274,7 +280,15 @@
1768592274523
-
+
+
+ 1768925413032
+
+
+
+ 1768925413032
+
+
@@ -308,16 +322,23 @@
-
+
+
- file://$PROJECT_DIR$/src/datasources/zabbix-hosts.ts
- 149
+ file://$PROJECT_DIR$/src/datasources/zabbix-request.ts
+ 133
-
+
+
+
+ file://$PROJECT_DIR$/src/datasources/zabbix-request.ts
+ 213
+
+
diff --git a/docs/sample_all_template_groups_query.graphql b/docs/sample_all_template_groups_query.graphql
new file mode 100644
index 0000000..5a608bc
--- /dev/null
+++ b/docs/sample_all_template_groups_query.graphql
@@ -0,0 +1,18 @@
+### Query
+Use this query to list all template groups.
+
+```graphql
+query AllTemplateGroups($name_pattern: String) {
+ allTemplateGroups(name_pattern: $name_pattern) {
+ groupid
+ name
+ }
+}
+```
+
+### Variables
+```json
+{
+ "name_pattern": "Templates/Roadwork/*"
+}
+```
diff --git a/docs/sample_delete_template_groups_mutation.graphql b/docs/sample_delete_template_groups_mutation.graphql
new file mode 100644
index 0000000..a5da82d
--- /dev/null
+++ b/docs/sample_delete_template_groups_mutation.graphql
@@ -0,0 +1,30 @@
+### Mutation
+Use this mutation to delete template groups by their numeric IDs or by a name pattern.
+
+```graphql
+mutation DeleteTemplateGroups($groupids: [Int!], $name_pattern: String) {
+ deleteTemplateGroups(groupids: $groupids, name_pattern: $name_pattern) {
+ id
+ message
+ error {
+ message
+ code
+ data
+ }
+ }
+}
+```
+
+### Variables (by ID)
+```json
+{
+ "groupids": [201]
+}
+```
+
+### Variables (by name pattern)
+```json
+{
+ "name_pattern": "Templates/Roadwork/%"
+}
+```
diff --git a/docs/sample_delete_templates_mutation.graphql b/docs/sample_delete_templates_mutation.graphql
new file mode 100644
index 0000000..37b3881
--- /dev/null
+++ b/docs/sample_delete_templates_mutation.graphql
@@ -0,0 +1,30 @@
+### Mutation
+Use this mutation to delete templates by their numeric IDs or by a name pattern.
+
+```graphql
+mutation DeleteTemplates($templateids: [Int!], $name_pattern: String) {
+ deleteTemplates(templateids: $templateids, name_pattern: $name_pattern) {
+ id
+ message
+ error {
+ message
+ code
+ data
+ }
+ }
+}
+```
+
+### Variables (by ID)
+```json
+{
+ "templateids": [501]
+}
+```
+
+### Variables (by name pattern)
+```json
+{
+ "name_pattern": "BT_DEVICE_TRACKER%"
+}
+```
diff --git a/docs/sample_import_template_groups_mutation.graphql b/docs/sample_import_template_groups_mutation.graphql
new file mode 100644
index 0000000..e9256fd
--- /dev/null
+++ b/docs/sample_import_template_groups_mutation.graphql
@@ -0,0 +1,59 @@
+### Mutation
+Use this mutation to import template groups.
+
+```graphql
+mutation ImportTemplateGroups($templateGroups: [CreateTemplateGroup!]!) {
+ importTemplateGroups(templateGroups: $templateGroups) {
+ groupName
+ groupid
+ message
+ error {
+ message
+ code
+ data
+ }
+ }
+}
+```
+
+### Variables
+This sample data is based on the `template_groups` from `src/testdata/templates/zbx_default_templates_vcr.yaml`.
+
+```json
+{
+ "templateGroups": [
+ {
+ "uuid": "43aab460fe444f18886b19948413b7e3",
+ "groupName": "Permissions/ConstructionSite"
+ },
+ {
+ "uuid": "376524057e094c07aaa0cf7f524849dc",
+ "groupName": "Templates/Roadwork/Controller"
+ },
+ {
+ "uuid": "7d83c76454564390bb0e34600780eaec",
+ "groupName": "Templates/Roadwork/Device-Capabilities"
+ },
+ {
+ "uuid": "48d5d2a18a08448c96a931b63bb2c97d",
+ "groupName": "Templates/Roadwork/Device-Capabilities/FLASH_ATTACHABLE"
+ },
+ {
+ "uuid": "785986b84892468ea2e92d912747b1d3",
+ "groupName": "Templates/Roadwork/Device-Capabilities/GEOLOCALIZABLE"
+ },
+ {
+ "uuid": "a4b79479e97a4b48972dcb476d45e55a",
+ "groupName": "Templates/Roadwork/Device-Capabilities/HAS_OPERATIONAL_DATA"
+ },
+ {
+ "uuid": "3604af8102644bee9dcaf0f9c1ee93a1",
+ "groupName": "Templates/Roadwork/Devices"
+ },
+ {
+ "uuid": "5ad0bd9e42a4487e869e9e41b38fe553",
+ "groupName": "Templates/Roadwork/DisplayLibrary"
+ }
+ ]
+}
+```
diff --git a/docs/sample_import_templates_mutation.graphql b/docs/sample_import_templates_mutation.graphql
new file mode 100644
index 0000000..4e41fec
--- /dev/null
+++ b/docs/sample_import_templates_mutation.graphql
@@ -0,0 +1,97 @@
+### Mutation
+Use this mutation to import templates along with their items, tags, and linked templates.
+
+```graphql
+mutation ImportTemplates($templates: [CreateTemplate!]!) {
+ importTemplates(templates: $templates) {
+ host
+ templateid
+ message
+ error {
+ message
+ code
+ data
+ }
+ }
+}
+```
+
+### Variables
+This sample data is based on the `BT_DEVICE_TRACKER` template from `src/testdata/templates/zbx_default_templates_vcr.yaml`.
+
+```json
+{
+ "templates": [
+ {
+ "uuid": "27474f627cb344b782a81c16d7e0c7d1",
+ "host": "BT_DEVICE_TRACKER",
+ "name": "BT_DEVICE_TRACKER",
+ "groupNames": ["Templates/Roadwork/Devices"],
+ "templates": [
+ { "name": "ROADWORK_DEVICE" }
+ ],
+ "tags": [
+ { "tag": "class", "value": "roadwork" },
+ { "tag": "deviceType", "value": "bt_device_tracker_generic" }
+ ],
+ "items": [
+ {
+ "uuid": "d4d3ec9f3ca940a39a721b6cfd2f3471",
+ "name": "location",
+ "type": 18,
+ "key": "location",
+ "value_type": 4,
+ "history": "2d",
+ "preprocessing": [
+ {
+ "type": 21,
+ "params": [
+ "var obj=JSON.parse(value);\n\nif (obj[\"isFiltered\"]) {\n throw \"Result is filtered\";\n return \"filtered\";\n}\n\nreturn value;"
+ ]
+ },
+ {
+ "type": 15,
+ "params": ["filtered"],
+ "error_handler": 1
+ }
+ ],
+ "master_item": {
+ "key": "mqtt.trap[deviceValue/location]"
+ }
+ },
+ {
+ "uuid": "380c4a7d752848cba3b5a59a0f9b13c0",
+ "name": "MQTT_LOCATION",
+ "type": 2,
+ "key": "mqtt.trap[deviceValue/location]",
+ "value_type": 4,
+ "history": "0"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Mapping Reference
+When converting from Zabbix YAML/XML exports, use the following numeric mappings for items and preprocessing:
+
+#### Item Type (`type`)
+- `2`: ZABBIX_TRAP (TRAP)
+- `18`: DEPENDANT_ITEM (DEPENDENT)
+- `21`: SIMULATOR_JAVASCRIPT (JAVASCRIPT)
+
+#### Value Type (`value_type`)
+- `0`: Float
+- `3`: Int (Numeric unsigned)
+- `4`: Text
+
+#### Preprocessing Type (`type`)
+- `12`: JSONPATH
+- `15`: NOT_MATCHES_REGEX
+- `21`: JAVASCRIPT
+
+#### Error Handler (`error_handler`)
+- `1`: DISCARD_VALUE
+- `2`: SET_VALUE
+- `3`: SET_ERROR
diff --git a/docs/sample_templates_query.graphql b/docs/sample_templates_query.graphql
new file mode 100644
index 0000000..6583ce6
--- /dev/null
+++ b/docs/sample_templates_query.graphql
@@ -0,0 +1,18 @@
+### Query
+Use this query to verify the results of the template import.
+
+```graphql
+query GetTemplates($name_pattern: String) {
+ templates(name_pattern: $name_pattern) {
+ templateid
+ name
+ }
+}
+```
+
+### Variables
+```json
+{
+ "name_pattern": "BT_DEVICE_TRACKER"
+}
+```
diff --git a/package.json b/package.json
index 8cab6b9..a59a819 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"compile": "tsc",
- "start": "nodemon --watch \"src/**\" --watch \"schema.graphql\" --ext \"ts,json\" --exec \"tsc & node --require ts-node/register --inspect --import tsx/esm ./src/index.ts\"",
+ "start": "nodemon --watch \"src/**\" --watch \"schema.graphql\" --ext \"ts,json\" --exec \"node --import tsx ./src/index.ts\"",
"prod": "npm run copy-schema && node ./dist/index.js",
"test": "jest --detectOpenHandles --forceExit --bail",
"codegen": "graphql-codegen --config codegen.ts --watch \"schema.graphql\"",
diff --git a/schema/mutations.graphql b/schema/mutations.graphql
index f2c6e2b..376709c 100644
--- a/schema/mutations.graphql
+++ b/schema/mutations.graphql
@@ -29,12 +29,152 @@ type Mutation {
importHosts(hosts: [CreateHost!]!):[ImportHostResponse!]
importUserRights(input: UserRightsInput!, dryRun: Boolean! = true): ImportUserRightsResult
+
+ """
+ (Mass) Import template groups
+ and assign them by groupid or name.
+
+ Return value: If no error occurs a groupid be returned for each created group,
+ otherwise the return object will contain an error message
+
+ Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ """
+ importTemplateGroups(templateGroups: [CreateTemplateGroup!]!):[CreateTemplateGroupResponse!]
+
+ """
+ (Mass) Import templates.
+
+ Return value: If no error occurs a templateid will be returned for each created template,
+ otherwise the return object will contain an error message.
+
+ Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ """
+ importTemplates(templates: [CreateTemplate!]!):[ImportTemplateResponse!]
+
+ """
+ Delete templates.
+
+ Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ """
+ deleteTemplates(templateids: [Int!], name_pattern: String): [DeleteResponse!]
+
+ """
+ Delete template groups.
+
+ Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ """
+ deleteTemplateGroups(groupids: [Int!], name_pattern: String): [DeleteResponse!]
}
####################################################################
# Input types used for importXXX - and storeXXX - Mutations
####################################################################
+type DeleteResponse {
+ id: Int!
+ message: String
+ error: ApiError
+}
+
+input CreateTemplateGroup {
+ """
+ Name of the template group
+ """
+ groupName: String!
+ """
+ Internally used unique id
+ (will be assigned by Zabbix if empty)
+ """
+ uuid: String
+}
+
+input CreateTemplate {
+ """
+ Name of the template
+ """
+ host: String!
+ """
+ Visible name of the template
+ """
+ name: String
+ """
+ groupNames is used to assign the created object
+ to a template group.
+ """
+ groupNames: [String!]!
+ """
+ Optionally the internal groupids can be passed - in this case the
+ groupName is ignored
+ """
+ groupids: [Int]
+ """
+ Internally used unique id
+ (will be assigned by Zabbix if empty)
+ """
+ uuid: String
+ """
+ Template items
+ """
+ items: [CreateTemplateItem!]
+ """
+ Linked templates
+ """
+ templates: [CreateLinkedTemplate!]
+ """
+ Template tags
+ """
+ tags: [CreateTag!]
+}
+
+input CreateTemplateItem {
+ uuid: String
+ name: String!
+ type: Int
+ key: String!
+ value_type: Int
+ history: String
+ units: String
+ delay: String
+ description: String
+ preprocessing: [CreateItemPreprocessing!]
+ tags: [CreateTag!]
+ master_item: CreateMasterItem
+}
+
+input CreateMasterItem {
+ key: String!
+}
+
+input CreateItemPreprocessing {
+ type: Int!
+ params: [String!]!
+ error_handler: Int
+ error_handler_params: String
+}
+
+input CreateLinkedTemplate {
+ name: String!
+}
+
+input CreateTag {
+ tag: String!
+ value: String
+}
+
+type ImportTemplateResponse {
+ host: String!
+ templateid: String
+ message: String
+ error: ApiError
+}
+
+type CreateTemplateGroupResponse {
+ groupName: String!
+ groupid: Int
+ message: String
+ error: ApiError
+}
+
input CreateHostGroup {
"""
Name of the host group
diff --git a/schema/queries.graphql b/schema/queries.graphql
index acd128c..781c6e2 100644
--- a/schema/queries.graphql
+++ b/schema/queries.graphql
@@ -113,5 +113,15 @@ type Query {
exclude_groups_pattern: Regex allowing to exclude all matching hostgroups from group permissions
"""
exportUserRights(name_pattern: String = "" exclude_hostgroups_pattern: String = ""): UserRights
+
+ """
+ Get templates.
+ """
+ templates(hostids: [Int], name_pattern: String): [Template]
+
+ """
+ Get template groups.
+ """
+ allTemplateGroups(name_pattern: String): [HostGroup]
}
diff --git a/src/api/resolvers.ts b/src/api/resolvers.ts
index 62225dc..da934a8 100644
--- a/src/api/resolvers.ts
+++ b/src/api/resolvers.ts
@@ -6,6 +6,10 @@ import {
MutationCreateHostArgs,
MutationImportHostGroupsArgs,
MutationImportHostsArgs,
+ MutationImportTemplateGroupsArgs,
+ MutationImportTemplatesArgs,
+ MutationDeleteTemplatesArgs,
+ MutationDeleteTemplateGroupsArgs,
MutationImportUserRightsArgs,
Permission, QueryAllDevicesArgs,
QueryAllHostGroupsArgs,
@@ -13,12 +17,15 @@ import {
QueryExportHostValueHistoryArgs,
QueryExportUserRightsArgs,
QueryHasPermissionsArgs,
+ QueryTemplatesArgs,
QueryUserPermissionsArgs,
Resolvers,
StorageItemType,
} from "../schema/generated/graphql.js";
import {HostImporter} from "../execution/host_importer.js";
+import {TemplateImporter} from "../execution/template_importer.js";
+import {TemplateDeleter} from "../execution/template_deleter.js";
import {HostValueExporter} from "../execution/host_exporter.js";
import {logger} from "../logging/logger.js";
import {ParsedArgs, ZabbixRequest} from "../datasources/zabbix-request.js";
@@ -39,6 +46,14 @@ import {
ZabbixImportUserRolesRequest,
ZabbixQueryUserRolesRequest
} from "../datasources/zabbix-userroles.js";
+import {
+ ZabbixCreateItemRequest,
+ ZabbixCreateTemplateGroupRequest,
+ ZabbixCreateTemplateRequest,
+ ZabbixQueryItemRequest,
+ ZabbixQueryTemplateGroupRequest,
+ ZabbixQueryTemplatesRequest
+} from "../datasources/zabbix-templates.js";
import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api.js";
import {GraphQLInterfaceType, GraphQLList} from "graphql/type/index.js";
import {isDevice} from "./resolver_helpers.js";
@@ -129,6 +144,37 @@ export function createResolvers(): Resolvers {
userGroups: groups,
userRoles: roles
}
+ },
+
+ templates: async (_parent: any, args: QueryTemplatesArgs, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ let params: any = {}
+ if (args.hostids) {
+ params.templateids = args.hostids
+ }
+ if (args.name_pattern) {
+ params.search = {
+ name: args.name_pattern
+ }
+ }
+ return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
+ .executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
+ },
+
+ allTemplateGroups: async (_parent: any, args: any, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ let params: any = {}
+ if (args.name_pattern) {
+ params.search = {
+ name: args.name_pattern
+ }
+ }
+ return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
+ .executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
}
},
Mutation: {
@@ -172,6 +218,30 @@ export function createResolvers(): Resolvers {
userRoles: userRolesImport,
userGroups: userGroupsImport
}
+ },
+ importTemplateGroups: async (_parent: any, args: MutationImportTemplateGroupsArgs, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ return TemplateImporter.importTemplateGroups(args.templateGroups, zabbixAuthToken, cookie)
+ },
+ importTemplates: async (_parent: any, args: MutationImportTemplatesArgs, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ return TemplateImporter.importTemplates(args.templates, zabbixAuthToken, cookie)
+ },
+ deleteTemplates: async (_parent: any, args: MutationDeleteTemplatesArgs, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ return TemplateDeleter.deleteTemplates(args.templateids, args.name_pattern, zabbixAuthToken, cookie)
+ },
+ deleteTemplateGroups: async (_parent: any, args: MutationDeleteTemplateGroupsArgs, {
+ zabbixAuthToken,
+ cookie
+ }: any) => {
+ return TemplateDeleter.deleteTemplateGroups(args.groupids, args.name_pattern, zabbixAuthToken, cookie)
}
},
diff --git a/src/datasources/zabbix-request.ts b/src/datasources/zabbix-request.ts
index ff77fc2..5fa8f93 100644
--- a/src/datasources/zabbix-request.ts
+++ b/src/datasources/zabbix-request.ts
@@ -39,7 +39,7 @@ export class ParsedArgs {
constructor(params?: any) {
if (Array.isArray(params)) {
this.zabbix_params = params.map(arg => this.parseArgObject(arg))
- } else if (params instanceof Object) {
+ } else {
this.zabbix_params = this.parseArgObject(params)
}
}
@@ -52,9 +52,12 @@ export class ParsedArgs {
return paramName in this.zabbix_params ? this.zabbix_params[paramName] : undefined
}
- parseArgObject(args?: Object) {
+ parseArgObject(args?: any) {
+ if (args && (typeof args !== 'object' || args.constructor !== Object)) {
+ return args;
+ }
let result: ZabbixParams
- if (args) {
+ if (args && typeof args === 'object' && args.constructor === Object) {
if ("name_pattern" in args && typeof args["name_pattern"] == "string") {
if (args["name_pattern"]) {
this.name_pattern = args["name_pattern"]
@@ -159,7 +162,10 @@ export class ZabbixRequest {
- return {...this.requestBodyTemplate.params, ...paramsObj}
+ if (paramsObj !== null && typeof paramsObj === 'object' && paramsObj.constructor === Object) {
+ return {...this.requestBodyTemplate.params, ...paramsObj}
+ }
+ return paramsObj;
})
} else {
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)}
diff --git a/src/datasources/zabbix-templates.ts b/src/datasources/zabbix-templates.ts
index d8e7ae4..9352010 100644
--- a/src/datasources/zabbix-templates.ts
+++ b/src/datasources/zabbix-templates.ts
@@ -1,5 +1,7 @@
-import { ZabbixRequest } from "./zabbix-request.js";
+import {isZabbixErrorResult, ParsedArgs, ZabbixRequest} from "./zabbix-request.js";
+import {ZabbixAPI} from "./zabbix-api.js";
+import {logger} from "../logging/logger.js";
@@ -30,3 +32,40 @@ export class ZabbixQueryTemplateGroupRequest extends ZabbixRequest {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("templategroup.create", authToken, cookie);
+ }
+}
+
+export class ZabbixCreateTemplateRequest extends ZabbixRequest<{ templateids: string[] }> {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("template.create", authToken, cookie);
+ }
+}
+
+export class ZabbixQueryItemRequest extends ZabbixRequest {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("item.get", authToken, cookie);
+ }
+}
+
+export class ZabbixCreateItemRequest extends ZabbixRequest<{ itemids: string[] }> {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("item.create", authToken, cookie);
+ }
+}
+
+export class ZabbixDeleteTemplatesRequest extends ZabbixRequest<{ templateids: string[] }> {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("template.delete", authToken, cookie);
+ }
+}
+
+export class ZabbixDeleteTemplateGroupsRequest extends ZabbixRequest<{ groupids: string[] }> {
+ constructor(authToken?: string | null, cookie?: string | null) {
+ super("templategroup.delete", authToken, cookie);
+ }
+}
+
+
diff --git a/src/execution/template_deleter.ts b/src/execution/template_deleter.ts
new file mode 100644
index 0000000..2eac02e
--- /dev/null
+++ b/src/execution/template_deleter.ts
@@ -0,0 +1,107 @@
+
+import {DeleteResponse} from "../schema/generated/graphql.js";
+import {
+ ZabbixDeleteTemplateGroupsRequest,
+ ZabbixDeleteTemplatesRequest,
+ ZabbixQueryTemplateGroupRequest,
+ ZabbixQueryTemplatesRequest
+} from "../datasources/zabbix-templates.js";
+import {isZabbixErrorResult, ParsedArgs} from "../datasources/zabbix-request.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+
+export class TemplateDeleter {
+
+ public static async deleteTemplates(templateids: number[] | null | undefined, name_pattern?: string | null, zabbixAuthToken?: string, cookie?: string): Promise {
+ const result: DeleteResponse[] = [];
+ let idsToDelete = templateids ? [...templateids] : [];
+
+ if (name_pattern) {
+ const queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs({ name_pattern: name_pattern }));
+
+ if (!isZabbixErrorResult(queryResult) && Array.isArray(queryResult)) {
+ const foundIds = queryResult.map(t => Number(t.templateid));
+ // Merge and deduplicate
+ idsToDelete = Array.from(new Set([...idsToDelete, ...foundIds]));
+ }
+ }
+
+ if (idsToDelete.length === 0) {
+ return [];
+ }
+
+ // Zabbix template.delete accepts an array of template IDs
+ const deleteResult = await new ZabbixDeleteTemplatesRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs(idsToDelete));
+
+ if (isZabbixErrorResult(deleteResult)) {
+ let errorMessage = deleteResult.error.message;
+ if (deleteResult.error.data) {
+ errorMessage += " " + (typeof deleteResult.error.data === 'string' ? deleteResult.error.data : JSON.stringify(deleteResult.error.data));
+ }
+ // If the whole batch fails, we report the error for each ID
+ for (const id of idsToDelete) {
+ result.push({
+ id: id,
+ message: errorMessage,
+ error: deleteResult.error
+ });
+ }
+ } else if (deleteResult?.templateids) {
+ for (const id of idsToDelete) {
+ result.push({
+ id: id,
+ message: `Template ${id} deleted successfully`
+ });
+ }
+ }
+
+ return result;
+ }
+
+ public static async deleteTemplateGroups(groupids: number[] | null | undefined, name_pattern?: string | null, zabbixAuthToken?: string, cookie?: string): Promise {
+ const result: DeleteResponse[] = [];
+ let idsToDelete = groupids ? [...groupids] : [];
+
+ if (name_pattern) {
+ const queryResult = await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs({ name_pattern: name_pattern }));
+
+ if (!isZabbixErrorResult(queryResult) && Array.isArray(queryResult)) {
+ const foundIds = queryResult.map(g => Number(g.groupid));
+ // Merge and deduplicate
+ idsToDelete = Array.from(new Set([...idsToDelete, ...foundIds]));
+ }
+ }
+
+ if (idsToDelete.length === 0) {
+ return [];
+ }
+
+ const deleteResult = await new ZabbixDeleteTemplateGroupsRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs(idsToDelete));
+
+ if (isZabbixErrorResult(deleteResult)) {
+ let errorMessage = deleteResult.error.message;
+ if (deleteResult.error.data) {
+ errorMessage += " " + (typeof deleteResult.error.data === 'string' ? deleteResult.error.data : JSON.stringify(deleteResult.error.data));
+ }
+ for (const id of idsToDelete) {
+ result.push({
+ id: id,
+ message: errorMessage,
+ error: deleteResult.error
+ });
+ }
+ } else if (deleteResult?.groupids) {
+ for (const id of idsToDelete) {
+ result.push({
+ id: id,
+ message: `Template group ${id} deleted successfully`
+ });
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/src/execution/template_importer.ts b/src/execution/template_importer.ts
new file mode 100644
index 0000000..5910ac1
--- /dev/null
+++ b/src/execution/template_importer.ts
@@ -0,0 +1,220 @@
+
+import {
+ CreateTemplate,
+ CreateTemplateGroup,
+ CreateTemplateGroupResponse,
+ ImportTemplateResponse,
+ InputMaybe
+} from "../schema/generated/graphql.js";
+import {logger} from "../logging/logger.js";
+import {
+ ZabbixCreateItemRequest,
+ ZabbixCreateTemplateGroupRequest,
+ ZabbixCreateTemplateRequest,
+ ZabbixQueryItemRequest,
+ ZabbixQueryTemplateGroupRequest,
+ ZabbixQueryTemplatesRequest
+} from "../datasources/zabbix-templates.js";
+import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+
+export class TemplateImporter {
+
+ public static async importTemplateGroups(templateGroups: InputMaybe> | undefined, zabbixAuthToken?: string, cookie?: string) {
+ if (!templateGroups) {
+ return null
+ }
+ let result: CreateTemplateGroupResponse[] = []
+ for (let group of templateGroups) {
+ let createGroupResult: { groupids: string[] } | ZabbixErrorResult | undefined = undefined;
+
+ // Try to find if it exists by name first
+ let groups = await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
+ filter_name: group.groupName
+ }))
+
+ let groupid = 0
+ let message: string | undefined = undefined
+
+ if (!isZabbixErrorResult(groups) && groups?.length) {
+ groupid = Number(groups[0].groupid)
+ message = `Template group ${group.groupName} already exists with groupid=${groupid} - skipping`
+ logger.debug(message)
+ } else {
+ createGroupResult = await new ZabbixCreateTemplateGroupRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs({
+ name: group.groupName,
+ uuid: group.uuid
+ }))
+
+ if (isZabbixErrorResult(createGroupResult)) {
+ let errorMessage = createGroupResult.error.message;
+ if (createGroupResult.error.data) {
+ errorMessage += " " + (typeof createGroupResult.error.data === 'string' ? createGroupResult.error.data : JSON.stringify(createGroupResult.error.data));
+ }
+ result.push({
+ groupName: group.groupName,
+ message: `Unable to create template group ${group.groupName}: ${errorMessage}`,
+ error: createGroupResult.error
+ })
+ continue
+ } else if (createGroupResult?.groupids?.length) {
+ groupid = Number(createGroupResult.groupids[0])
+ }
+ }
+
+ if (groupid) {
+ result.push({
+ groupName: group.groupName,
+ groupid: groupid,
+ message: message
+ })
+ } else {
+ result.push({
+ groupName: group.groupName,
+ message: `Unable to create template group ${group.groupName}: Unknown error`,
+ error: { message: "Unknown error - no groupid returned" }
+ })
+ }
+ }
+ return result
+ }
+
+ public static async importTemplates(templates: InputMaybe> | undefined, zabbixAuthToken?: string, cookie?: string) {
+ if (!templates) {
+ return null
+ }
+ let result: ImportTemplateResponse[] = []
+ for (let template of templates) {
+ // 1. Resolve Group IDs
+ let groupids = template.groupids
+ if (!groupids || groupids.length === 0) {
+ let groups = await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
+ filter_name: template.groupNames
+ }))
+
+ if (isZabbixErrorResult(groups) || !groups?.length) {
+ result.push({
+ host: template.host,
+ message: `Unable to find template groups=${template.groupNames}`
+ })
+ continue
+ }
+ groupids = groups.map(g => Number(g.groupid))
+ }
+
+ // 2. Resolve Linked Templates IDs
+ let linkedTemplates: { templateid: string }[] = []
+ if (template.templates && template.templates.length > 0) {
+ let templateNames = template.templates.map(t => t.name)
+ let queryResult = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
+ filter_host: templateNames
+ }))
+
+ if (isZabbixErrorResult(queryResult)) {
+ let errorMessage = queryResult.error.message;
+ if (queryResult.error.data) {
+ errorMessage += " " + (typeof queryResult.error.data === 'string' ? queryResult.error.data : JSON.stringify(queryResult.error.data));
+ }
+ result.push({
+ host: template.host,
+ message: `Error querying linked templates: ${errorMessage}`,
+ error: queryResult.error
+ })
+ continue
+ }
+ linkedTemplates = queryResult.map(t => ({ templateid: t.templateid }))
+ }
+
+ // 3. Create Template
+ let templateCreateParams: any = {
+ host: template.host,
+ name: template.name || template.host,
+ groups: groupids.map(id => ({ groupid: id })),
+ uuid: template.uuid,
+ templates: linkedTemplates,
+ tags: template.tags?.map(t => ({ tag: t.tag, value: t.value || "" }))
+ }
+
+ let templateImportResult = await new ZabbixCreateTemplateRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs(templateCreateParams))
+
+ if (isZabbixErrorResult(templateImportResult) || !templateImportResult?.templateids?.length) {
+ let errorMessage = isZabbixErrorResult(templateImportResult) ? templateImportResult.error.message : "Unknown error";
+ if (isZabbixErrorResult(templateImportResult) && templateImportResult.error.data) {
+ errorMessage += " " + (typeof templateImportResult.error.data === 'string' ? templateImportResult.error.data : JSON.stringify(templateImportResult.error.data));
+ }
+ result.push({
+ host: template.host,
+ message: `Unable to import template=${template.host}: ${errorMessage}`,
+ error: isZabbixErrorResult(templateImportResult) ? templateImportResult.error : undefined
+ })
+ continue
+ }
+
+ let templateid = templateImportResult.templateids[0]
+
+ // 4. Create Items if any
+ if (template.items && template.items.length > 0) {
+ const createdItemKeyToId = new Map();
+ let itemsToCreate = [...template.items];
+ let retry = true;
+
+ while (retry && itemsToCreate.length > 0) {
+ retry = false;
+ const remainingItems: typeof itemsToCreate = [];
+
+ for (let item of itemsToCreate) {
+ if (item.master_item && !createdItemKeyToId.has(item.master_item.key)) {
+ remainingItems.push(item);
+ continue;
+ }
+
+ let { key, master_item, ...itemData } = item;
+ let itemCreateParams: any = {
+ ...itemData,
+ key_: key,
+ hostid: templateid,
+ preprocessing: item.preprocessing?.map(p => ({
+ type: p.type,
+ params: p.params.join("\n"),
+ error_handler: p.error_handler,
+ error_handler_params: p.error_handler_params
+ })),
+ tags: item.tags?.map(t => ({ tag: t.tag, value: t.value || "" }))
+ }
+
+ if (master_item) {
+ itemCreateParams.master_itemid = createdItemKeyToId.get(master_item.key);
+ }
+
+ let itemResult = await new ZabbixCreateItemRequest(zabbixAuthToken, cookie)
+ .executeRequestReturnError(zabbixAPI, new ParsedArgs(itemCreateParams))
+
+ if (isZabbixErrorResult(itemResult)) {
+ let errorMessage = itemResult.error.message;
+ if (itemResult.error.data) {
+ errorMessage += " " + (typeof itemResult.error.data === 'string' ? itemResult.error.data : JSON.stringify(itemResult.error.data));
+ }
+ logger.error(`Unable to create item ${item.name} for template ${template.host}: ${errorMessage}`)
+ } else if (itemResult?.itemids?.length) {
+ createdItemKeyToId.set(key, itemResult.itemids[0]);
+ retry = true;
+ }
+ }
+ itemsToCreate = remainingItems;
+ }
+
+ if (itemsToCreate.length > 0) {
+ logger.error(`Unable to create ${itemsToCreate.length} items for template ${template.host} due to missing master items: ${itemsToCreate.map(i => i.name).join(", ")}`);
+ }
+ }
+
+ result.push({
+ host: template.host,
+ templateid: templateid
+ })
+ }
+ return result
+ }
+}
diff --git a/src/schema/generated/graphql.ts b/src/schema/generated/graphql.ts
index 737a5ab..0d155af 100644
--- a/src/schema/generated/graphql.ts
+++ b/src/schema/generated/graphql.ts
@@ -81,6 +81,94 @@ export interface CreateHostResponse {
itemids?: Maybe>>;
}
+export interface CreateItemPreprocessing {
+ error_handler?: InputMaybe;
+ error_handler_params?: InputMaybe;
+ params: Array;
+ type: Scalars['Int']['input'];
+}
+
+export interface CreateLinkedTemplate {
+ name: Scalars['String']['input'];
+}
+
+export interface CreateMasterItem {
+ key: Scalars['String']['input'];
+}
+
+export interface CreateTag {
+ tag: Scalars['String']['input'];
+ value?: InputMaybe;
+}
+
+export interface CreateTemplate {
+ /**
+ * groupNames is used to assign the created object
+ * to a template group.
+ */
+ groupNames: Array;
+ /**
+ * Optionally the internal groupids can be passed - in this case the
+ * groupName is ignored
+ */
+ groupids?: InputMaybe>>;
+ /** Name of the template */
+ host: Scalars['String']['input'];
+ /** Template items */
+ items?: InputMaybe>;
+ /** Visible name of the template */
+ name?: InputMaybe;
+ /** Template tags */
+ tags?: InputMaybe>;
+ /** Linked templates */
+ templates?: InputMaybe>;
+ /**
+ * Internally used unique id
+ * (will be assigned by Zabbix if empty)
+ */
+ uuid?: InputMaybe;
+}
+
+export interface CreateTemplateGroup {
+ /** Name of the template group */
+ groupName: Scalars['String']['input'];
+ /**
+ * Internally used unique id
+ * (will be assigned by Zabbix if empty)
+ */
+ uuid?: InputMaybe;
+}
+
+export interface CreateTemplateGroupResponse {
+ __typename?: 'CreateTemplateGroupResponse';
+ error?: Maybe;
+ groupName: Scalars['String']['output'];
+ groupid?: Maybe;
+ message?: Maybe;
+}
+
+export interface CreateTemplateItem {
+ delay?: InputMaybe;
+ description?: InputMaybe;
+ history?: InputMaybe;
+ key: Scalars['String']['input'];
+ master_item?: InputMaybe;
+ name: Scalars['String']['input'];
+ preprocessing?: InputMaybe>;
+ tags?: InputMaybe>;
+ type?: InputMaybe;
+ units?: InputMaybe;
+ uuid?: InputMaybe;
+ value_type?: InputMaybe;
+}
+
+export interface DeleteResponse {
+ __typename?: 'DeleteResponse';
+ error?: Maybe;
+ id: Scalars['Int']['output'];
+ message?: Maybe;
+}
+
/**
* (IoT / Edge - ) Devices are hosts having a state containing the "output" / the business data which is exposed
* besides monitoring information.
@@ -232,6 +320,14 @@ export interface ImportHostResponse {
message?: Maybe;
}
+export interface ImportTemplateResponse {
+ __typename?: 'ImportTemplateResponse';
+ error?: Maybe;
+ host: Scalars['String']['output'];
+ message?: Maybe;
+ templateid?: Maybe;
+}
+
export interface ImportUserRightResult {
__typename?: 'ImportUserRightResult';
errors?: Maybe>;
@@ -268,6 +364,18 @@ export interface Mutation {
__typename?: 'Mutation';
/** Authentication: By zbx_session - cookie or zabbix-auth-token - header */
createHost?: Maybe;
+ /**
+ * Delete template groups.
+ *
+ * Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ */
+ deleteTemplateGroups?: Maybe>;
+ /**
+ * Delete templates.
+ *
+ * Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ */
+ deleteTemplates?: Maybe>;
/**
* (Mass) Import zabbix groups
* and assign them to the corresponding hosts by groupid or groupName.
@@ -287,6 +395,25 @@ export interface Mutation {
* Authentication: By zbx_session - cookie or zabbix-auth-token - header
*/
importHosts?: Maybe>;
+ /**
+ * (Mass) Import template groups
+ * and assign them by groupid or name.
+ *
+ * Return value: If no error occurs a groupid be returned for each created group,
+ * otherwise the return object will contain an error message
+ *
+ * Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ */
+ importTemplateGroups?: Maybe>;
+ /**
+ * (Mass) Import templates.
+ *
+ * Return value: If no error occurs a templateid will be returned for each created template,
+ * otherwise the return object will contain an error message.
+ *
+ * Authentication: By zbx_session - cookie or zabbix-auth-token - header
+ */
+ importTemplates?: Maybe>;
importUserRights?: Maybe;
}
@@ -299,6 +426,18 @@ export interface MutationCreateHostArgs {
}
+export interface MutationDeleteTemplateGroupsArgs {
+ groupids?: InputMaybe>;
+ name_pattern?: InputMaybe;
+}
+
+
+export interface MutationDeleteTemplatesArgs {
+ name_pattern?: InputMaybe;
+ templateids?: InputMaybe>;
+}
+
+
export interface MutationImportHostGroupsArgs {
hostGroups: Array;
}
@@ -309,6 +448,16 @@ export interface MutationImportHostsArgs {
}
+export interface MutationImportTemplateGroupsArgs {
+ templateGroups: Array;
+}
+
+
+export interface MutationImportTemplatesArgs {
+ templates: Array;
+}
+
+
export interface MutationImportUserRightsArgs {
dryRun?: Scalars['Boolean']['input'];
input: UserRightsInput;
@@ -359,6 +508,8 @@ export interface Query {
* Authentication: By zbx_session - cookie or zabbix-auth-token - header
*/
allHosts?: Maybe>>;
+ /** Get template groups. */
+ allTemplateGroups?: Maybe>>;
/** Get api (build) version */
apiVersion: Scalars['String']['output'];
/**
@@ -401,6 +552,8 @@ export interface Query {
* operation. Returns true on success
*/
logout?: Maybe;
+ /** Get templates. */
+ templates?: Maybe>>;
/**
* Return all user permissions. If objectNames is provided return only the permissions related to the objects within
* the objectNames - list
@@ -439,6 +592,11 @@ export interface QueryAllHostsArgs {
}
+export interface QueryAllTemplateGroupsArgs {
+ name_pattern?: InputMaybe;
+}
+
+
export interface QueryExportHostValueHistoryArgs {
host_filter?: InputMaybe>;
itemKey_filter?: InputMaybe>;
@@ -474,6 +632,12 @@ export interface QueryLoginArgs {
}
+export interface QueryTemplatesArgs {
+ hostids?: InputMaybe>>;
+ name_pattern?: InputMaybe;
+}
+
+
export interface QueryUserPermissionsArgs {
objectNames?: InputMaybe>;
}
@@ -749,7 +913,16 @@ export type ResolversTypes = {
CreateHostGroup: CreateHostGroup;
CreateHostGroupResponse: ResolverTypeWrapper;
CreateHostResponse: ResolverTypeWrapper;
+ CreateItemPreprocessing: CreateItemPreprocessing;
+ CreateLinkedTemplate: CreateLinkedTemplate;
+ CreateMasterItem: CreateMasterItem;
+ CreateTag: CreateTag;
+ CreateTemplate: CreateTemplate;
+ CreateTemplateGroup: CreateTemplateGroup;
+ CreateTemplateGroupResponse: ResolverTypeWrapper;
+ CreateTemplateItem: CreateTemplateItem;
DateTime: ResolverTypeWrapper;
+ DeleteResponse: ResolverTypeWrapper;
Device: ResolverTypeWrapper['Device']>;
DeviceCommunicationType: DeviceCommunicationType;
DeviceConfig: ResolverTypeWrapper;
@@ -769,6 +942,7 @@ export type ResolversTypes = {
HostGroup: ResolverTypeWrapper;
ID: ResolverTypeWrapper;
ImportHostResponse: ResolverTypeWrapper;
+ ImportTemplateResponse: ResolverTypeWrapper;
ImportUserRightResult: ResolverTypeWrapper;
ImportUserRightsResult: ResolverTypeWrapper;
Int: ResolverTypeWrapper;
@@ -814,7 +988,16 @@ export type ResolversParentTypes = {
CreateHostGroup: CreateHostGroup;
CreateHostGroupResponse: CreateHostGroupResponse;
CreateHostResponse: CreateHostResponse;
+ CreateItemPreprocessing: CreateItemPreprocessing;
+ CreateLinkedTemplate: CreateLinkedTemplate;
+ CreateMasterItem: CreateMasterItem;
+ CreateTag: CreateTag;
+ CreateTemplate: CreateTemplate;
+ CreateTemplateGroup: CreateTemplateGroup;
+ CreateTemplateGroupResponse: CreateTemplateGroupResponse;
+ CreateTemplateItem: CreateTemplateItem;
DateTime: Scalars['DateTime']['output'];
+ DeleteResponse: DeleteResponse;
Device: ResolversInterfaceTypes['Device'];
DeviceConfig: DeviceConfig;
DeviceState: ResolversInterfaceTypes['DeviceState'];
@@ -832,6 +1015,7 @@ export type ResolversParentTypes = {
HostGroup: HostGroup;
ID: Scalars['ID']['output'];
ImportHostResponse: ImportHostResponse;
+ ImportTemplateResponse: ImportTemplateResponse;
ImportUserRightResult: ImportUserRightResult;
ImportUserRightsResult: ImportUserRightsResult;
Int: Scalars['Int']['output'];
@@ -890,10 +1074,25 @@ export type CreateHostResponseResolvers;
};
+export type CreateTemplateGroupResponseResolvers = {
+ error?: Resolver, ParentType, ContextType>;
+ groupName?: Resolver;
+ groupid?: Resolver, ParentType, ContextType>;
+ message?: Resolver, ParentType, ContextType>;
+ __isTypeOf?: IsTypeOfResolverFn;
+};
+
export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig {
name: 'DateTime';
}
+export type DeleteResponseResolvers = {
+ error?: Resolver, ParentType, ContextType>;
+ id?: Resolver;
+ message?: Resolver, ParentType, ContextType>;
+ __isTypeOf?: IsTypeOfResolverFn;
+};
+
export type DeviceResolvers = {
__resolveType: TypeResolveFn<'GenericDevice', ParentType, ContextType>;
deviceType?: Resolver, ParentType, ContextType>;
@@ -1011,6 +1210,14 @@ export type ImportHostResponseResolvers;
};
+export type ImportTemplateResponseResolvers = {
+ error?: Resolver, ParentType, ContextType>;
+ host?: Resolver;
+ message?: Resolver, ParentType, ContextType>;
+ templateid?: Resolver, ParentType, ContextType>;
+ __isTypeOf?: IsTypeOfResolverFn;
+};
+
export type ImportUserRightResultResolvers = {
errors?: Resolver>, ParentType, ContextType>;
id?: Resolver, ParentType, ContextType>;
@@ -1043,8 +1250,12 @@ export type LocationResolvers = {
createHost?: Resolver, ParentType, ContextType, RequireFields>;
+ deleteTemplateGroups?: Resolver>, ParentType, ContextType, Partial>;
+ deleteTemplates?: Resolver>, ParentType, ContextType, Partial>;
importHostGroups?: Resolver>, ParentType, ContextType, RequireFields>;
importHosts?: Resolver>, ParentType, ContextType, RequireFields>;
+ importTemplateGroups?: Resolver>, ParentType, ContextType, RequireFields>;
+ importTemplates?: Resolver>, ParentType, ContextType, RequireFields>;
importUserRights?: Resolver, ParentType, ContextType, RequireFields>;
};
@@ -1064,6 +1275,7 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>;
allHostGroups?: Resolver>>, ParentType, ContextType, RequireFields>;
allHosts?: Resolver>>, ParentType, ContextType, RequireFields>;
+ allTemplateGroups?: Resolver>>, ParentType, ContextType, Partial>;
apiVersion?: Resolver;
exportHostValueHistory?: Resolver, ParentType, ContextType, RequireFields>;
exportUserRights?: Resolver, ParentType, ContextType, RequireFields>;
@@ -1071,6 +1283,7 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>;
login?: Resolver, ParentType, ContextType, RequireFields>;
logout?: Resolver, ParentType, ContextType>;
+ templates?: Resolver>>, ParentType, ContextType, Partial>;
userPermissions?: Resolver>, ParentType, ContextType, Partial>;
zabbixVersion?: Resolver, ParentType, ContextType>;
};
@@ -1193,7 +1406,9 @@ export type Resolvers = {
ApiError?: ApiErrorResolvers;
CreateHostGroupResponse?: CreateHostGroupResponseResolvers;
CreateHostResponse?: CreateHostResponseResolvers;
+ CreateTemplateGroupResponse?: CreateTemplateGroupResponseResolvers;
DateTime?: GraphQLScalarType;
+ DeleteResponse?: DeleteResponseResolvers;
Device?: DeviceResolvers;
DeviceCommunicationType?: DeviceCommunicationTypeResolvers;
DeviceConfig?: DeviceConfigResolvers;
@@ -1211,6 +1426,7 @@ export type Resolvers = {
Host?: HostResolvers;
HostGroup?: HostGroupResolvers;
ImportHostResponse?: ImportHostResponseResolvers;
+ ImportTemplateResponse?: ImportTemplateResponseResolvers;
ImportUserRightResult?: ImportUserRightResultResolvers;
ImportUserRightsResult?: ImportUserRightsResultResolvers;
Inventory?: InventoryResolvers;
diff --git a/src/test/template_deleter.test.ts b/src/test/template_deleter.test.ts
new file mode 100644
index 0000000..3e0f1c5
--- /dev/null
+++ b/src/test/template_deleter.test.ts
@@ -0,0 +1,162 @@
+
+import {TemplateDeleter} from "../execution/template_deleter.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+
+// Mocking ZabbixAPI
+jest.mock("../datasources/zabbix-api.js", () => ({
+ zabbixAPI: {
+ executeRequest: jest.fn(),
+ post: jest.fn()
+ }
+}));
+
+describe("TemplateDeleter", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("deleteTemplates - success", async () => {
+ const templateids = [1, 2];
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["1", "2"] });
+
+ const result = await TemplateDeleter.deleteTemplates(templateids, null, "token");
+
+ expect(result).toHaveLength(2);
+ expect(result[0].id).toBe(1);
+ expect(result[0].message).toContain("deleted successfully");
+ expect(result[1].id).toBe(2);
+
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.delete", expect.objectContaining({
+ body: expect.objectContaining({
+ params: [1, 2]
+ })
+ }));
+ });
+
+ test("deleteTemplates - error", async () => {
+ const templateids = [1];
+ const zabbixError = {
+ error: {
+ code: -32602,
+ message: "Invalid params.",
+ data: "Template does not exist"
+ }
+ };
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(zabbixError);
+
+ const result = await TemplateDeleter.deleteTemplates(templateids, null, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result[0].error).toBeDefined();
+ expect(result[0].message).toContain("Invalid params.");
+ expect(result[0].message).toContain("Template does not exist");
+ });
+
+ test("deleteTemplates - by name_pattern", async () => {
+ // Mock template.get
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([
+ { templateid: "10", host: "PatternTemplate 1" },
+ { templateid: "11", host: "PatternTemplate 2" }
+ ]);
+ // Mock template.delete
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["10", "11"] });
+
+ const result = await TemplateDeleter.deleteTemplates(null, "PatternTemplate%", "token");
+
+ expect(result).toHaveLength(2);
+ expect(result.map(r => r.id)).toContain(10);
+ expect(result.map(r => r.id)).toContain(11);
+
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.get", expect.objectContaining({
+ body: expect.objectContaining({
+ params: expect.objectContaining({
+ search: { name: "PatternTemplate%" }
+ })
+ })
+ }));
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.delete", expect.objectContaining({
+ body: expect.objectContaining({
+ params: expect.arrayContaining([10, 11])
+ })
+ }));
+ });
+
+ test("deleteTemplates - merged IDs and name_pattern", async () => {
+ // Mock template.get
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([
+ { templateid: "10", host: "PatternTemplate 1" },
+ { templateid: "12", host: "PatternTemplate 3" }
+ ]);
+ // Mock template.delete
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["10", "11", "12"] });
+
+ const result = await TemplateDeleter.deleteTemplates([11], "PatternTemplate%", "token");
+
+ expect(result).toHaveLength(3);
+ expect(result.map(r => r.id)).toContain(10);
+ expect(result.map(r => r.id)).toContain(11);
+ expect(result.map(r => r.id)).toContain(12);
+
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.delete", expect.objectContaining({
+ body: expect.objectContaining({
+ params: expect.arrayContaining([10, 11, 12])
+ })
+ }));
+ });
+
+ test("deleteTemplateGroups - success", async () => {
+ const groupids = [101];
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ groupids: ["101"] });
+
+ const result = await TemplateDeleter.deleteTemplateGroups(groupids, null, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe(101);
+ expect(result[0].message).toContain("deleted successfully");
+ });
+
+ test("deleteTemplateGroups - error", async () => {
+ const groupids = [101];
+ const zabbixError = {
+ error: {
+ code: -32602,
+ message: "Invalid params.",
+ data: "Group is in use"
+ }
+ };
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(zabbixError);
+
+ const result = await TemplateDeleter.deleteTemplateGroups(groupids, null, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result[0].error).toBeDefined();
+ expect(result[0].message).toContain("Group is in use");
+ });
+
+ test("deleteTemplateGroups - by name_pattern", async () => {
+ // Mock templategroup.get
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([
+ { groupid: "201", name: "PatternGroup 1" }
+ ]);
+ // Mock templategroup.delete
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ groupids: ["201"] });
+
+ const result = await TemplateDeleter.deleteTemplateGroups(null, "PatternGroup%", "token");
+
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe(201);
+
+ expect(zabbixAPI.post).toHaveBeenCalledWith("templategroup.get", expect.objectContaining({
+ body: expect.objectContaining({
+ params: expect.objectContaining({
+ search: { name: "PatternGroup%" }
+ })
+ })
+ }));
+ expect(zabbixAPI.post).toHaveBeenCalledWith("templategroup.delete", expect.objectContaining({
+ body: expect.objectContaining({
+ params: [201]
+ })
+ }));
+ });
+});
diff --git a/src/test/template_importer.test.ts b/src/test/template_importer.test.ts
new file mode 100644
index 0000000..9457bbd
--- /dev/null
+++ b/src/test/template_importer.test.ts
@@ -0,0 +1,176 @@
+
+import {TemplateImporter} from "../execution/template_importer.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+import {
+ ZabbixCreateItemRequest,
+ ZabbixCreateTemplateGroupRequest,
+ ZabbixCreateTemplateRequest,
+ ZabbixQueryItemRequest,
+ ZabbixQueryTemplateGroupRequest,
+ ZabbixQueryTemplatesRequest
+} from "../datasources/zabbix-templates.js";
+import {ZabbixErrorResult} from "../datasources/zabbix-request.js";
+
+// Mocking ZabbixAPI.executeRequest
+jest.mock("../datasources/zabbix-api.js", () => ({
+ zabbixAPI: {
+ executeRequest: jest.fn(),
+ post: jest.fn()
+ }
+}));
+
+describe("TemplateImporter", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("importTemplateGroups - create new group", async () => {
+ const templateGroups = [{ groupName: "New Group", uuid: "uuid1" }];
+
+ // Mocking group.get to return empty (group doesn't exist)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
+ // Mocking group.create
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ groupids: ["101"] });
+
+ const result = await TemplateImporter.importTemplateGroups(templateGroups, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result![0].groupid).toBe(101);
+ expect(result![0].groupName).toBe("New Group");
+ });
+
+ test("importTemplateGroups - group already exists", async () => {
+ const templateGroups = [{ groupName: "Existing Group" }];
+
+ // Mocking group.get to return existing group
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "102", name: "Existing Group" }]);
+
+ const result = await TemplateImporter.importTemplateGroups(templateGroups, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result![0].groupid).toBe(102);
+ expect(result![0].message).toContain("already exists");
+ });
+
+ test("importTemplates - basic template", async () => {
+ const templates = [{
+ host: "Test Template",
+ groupNames: ["Group1"]
+ }];
+
+ // Mocking group.get for Group1
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Group1" }]);
+ // Mocking template.create
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["301"] });
+
+ const result = await TemplateImporter.importTemplates(templates, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result![0].templateid).toBe("301");
+ expect(result![0].host).toBe("Test Template");
+ });
+
+ test("importTemplates - with items, linked templates and dependent items", async () => {
+ const templates = [{
+ host: "Complex Template",
+ groupNames: ["Group1"],
+ templates: [{ name: "Linked Template" }],
+ items: [
+ {
+ name: "Dependent Item",
+ key: "dependent.key",
+ type: 18,
+ value_type: 3,
+ master_item: { key: "master.key" }
+ },
+ {
+ name: "Master Item",
+ key: "master.key",
+ type: 0,
+ value_type: 3,
+ }
+ ]
+ }];
+
+ // Mocking group.get
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Group1" }]);
+ // Mocking template.get for linked template
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "401", host: "Linked Template" }]);
+ // Mocking template.create
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["501"] });
+
+ // Mocking item.create for Master Item (first pass will pick Master Item because Dependent Item is missing its master)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["601"] });
+ // Mocking item.create for Dependent Item (second pass)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["602"] });
+
+ const result = await TemplateImporter.importTemplates(templates, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result![0].templateid).toBe("501");
+
+ // Check template.create params
+ const templateCreateCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "template.create");
+ expect(templateCreateCall[1].body.params.templates).toContainEqual({ templateid: "401" });
+
+ // Check item.create calls
+ const itemCreateCalls = (zabbixAPI.post as jest.Mock).mock.calls.filter(call => call[1].body.method === "item.create");
+ expect(itemCreateCalls).toHaveLength(2);
+
+ const masterCall = itemCreateCalls.find(c => c[1].body.params.name === "Master Item");
+ const dependentCall = itemCreateCalls.find(c => c[1].body.params.name === "Dependent Item");
+
+ expect(masterCall[1].body.params.key_).toBe("master.key");
+ expect(dependentCall[1].body.params.key_).toBe("dependent.key");
+ expect(dependentCall[1].body.params.master_itemid).toBe("601");
+ });
+
+ test("importTemplates - template query", async () => {
+ // This tests the template.get functionality used during import
+ const templates = [{
+ host: "Template A",
+ groupNames: ["Group1"],
+ templates: [{ name: "Template B" }]
+ }];
+
+ // Mock Group
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "1", name: "Group1" }]);
+ // Mock Template B lookup
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "2", host: "Template B" }]);
+ // Mock Template A creation
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["3"] });
+
+ await TemplateImporter.importTemplates(templates, "token");
+
+ // Verify that template.get was called for Template B
+ const templateQueryCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "template.get");
+ expect(templateQueryCall).toBeDefined();
+ expect(templateQueryCall[1].body.params.filter.host).toContain("Template B");
+ });
+
+ test("importTemplates - error message includes data field", async () => {
+ const templates = [{
+ host: "Error Template",
+ groupNames: ["Group1"]
+ }];
+
+ // Mocking group.get
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Group1" }]);
+
+ // Mocking template.create with an error including data
+ const zabbixError = {
+ error: {
+ code: -32602,
+ message: "Invalid params.",
+ data: "Invalid parameter \"/1\": the parameter \"key_\" is missing."
+ }
+ };
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(zabbixError);
+
+ const result = await TemplateImporter.importTemplates(templates, "token");
+
+ expect(result).toHaveLength(1);
+ expect(result![0].message).toContain("Invalid params.");
+ expect(result![0].message).toContain("the parameter \"key_\" is missing.");
+ });
+});
diff --git a/src/test/template_integration.test.ts b/src/test/template_integration.test.ts
new file mode 100644
index 0000000..a6c30fc
--- /dev/null
+++ b/src/test/template_integration.test.ts
@@ -0,0 +1,229 @@
+import { ApolloServer } from '@apollo/server';
+import { schema_loader } from '../api/schema.js';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import { zabbixAPI } from '../datasources/zabbix-api.js';
+
+// Mocking ZabbixAPI.post
+jest.mock("../datasources/zabbix-api.js", () => ({
+ zabbixAPI: {
+ post: jest.fn(),
+ executeRequest: jest.fn(),
+ baseURL: 'http://localhost/zabbix'
+ }
+}));
+
+describe("Template Integration Tests", () => {
+ let server: ApolloServer;
+
+ beforeAll(async () => {
+ const schema = await schema_loader();
+ server = new ApolloServer({
+ schema,
+ });
+ });
+
+ test("Import templates using sample query and variables", async () => {
+ const sampleFile = readFileSync(join(process.cwd(), 'docs', 'sample_import_templates_mutation.graphql'), 'utf-8').replace(/\r\n/g, '\n');
+
+ // Extract mutation and variables from the doc file
+ const mutationMatch = sampleFile.match(/```graphql\n([\s\S]*?)\n```/);
+ const variablesMatch = sampleFile.match(/```json\n([\s\S]*?)\n```/);
+
+ const mutation = mutationMatch![1];
+ const variables = JSON.parse(variablesMatch![1]);
+
+ // Mock Zabbix API calls
+ // 1. Group lookup
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Templates/Roadwork/Devices" }]);
+ // 2. Linked template lookup
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "401", host: "ROADWORK_DEVICE" }]);
+ // 3. Template creation
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["501"] });
+ // 4. Item creation (location)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["601"] });
+ // 5. Item creation (MQTT_LOCATION)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["602"] });
+
+ const response = await server.executeOperation({
+ query: mutation,
+ variables: variables,
+ }, {
+ contextValue: {
+ zabbixAuthToken: 'test-token'
+ }
+ });
+
+ expect(response.body.kind).toBe('single');
+ // @ts-ignore
+ const result = response.body.singleResult;
+ expect(result.errors).toBeUndefined();
+ expect(result.data.importTemplates).toHaveLength(1);
+ expect(result.data.importTemplates[0].host).toBe("BT_DEVICE_TRACKER");
+ expect(result.data.importTemplates[0].templateid).toBe("501");
+ });
+
+ test("Import and Export templates comparison", async () => {
+ // 1. Import
+ const importSample = readFileSync(join(process.cwd(), 'docs', 'sample_import_templates_mutation.graphql'), 'utf-8').replace(/\r\n/g, '\n');
+ const importMutation = importSample.match(/```graphql\n([\s\S]*?)\n```/)![1];
+ const importVariables = JSON.parse(importSample.match(/```json\n([\s\S]*?)\n```/)![1]);
+
+ // Mock for import
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ groupid: "201", name: "Templates/Roadwork/Devices" }]);
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "401", host: "ROADWORK_DEVICE" }]);
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["501"] });
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["601"] });
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ itemids: ["602"] });
+
+ await server.executeOperation({
+ query: importMutation,
+ variables: importVariables,
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ // 2. Export (Query)
+ const querySample = readFileSync(join(process.cwd(), 'docs', 'sample_templates_query.graphql'), 'utf-8').replace(/\r\n/g, '\n');
+ const query = querySample.match(/```graphql\n([\s\S]*?)\n```/)![1];
+ const queryVariables = JSON.parse(querySample.match(/```json\n([\s\S]*?)\n```/)![1]);
+
+ // Mock for query
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "501", host: "BT_DEVICE_TRACKER", name: "BT_DEVICE_TRACKER" }]);
+
+ const queryResponse = await server.executeOperation({
+ query: query,
+ variables: queryVariables,
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ expect(queryResponse.body.kind).toBe('single');
+ // @ts-ignore
+ const queryResult = queryResponse.body.singleResult;
+ expect(queryResult.errors).toBeUndefined();
+ expect(queryResult.data.templates).toHaveLength(1);
+ expect(queryResult.data.templates[0].name).toBe(importVariables.templates[0].name);
+ expect(queryResult.data.templates[0].templateid).toBe("501");
+
+ // 3. Delete
+ const deleteMutation = `
+ mutation DeleteTemplates($templateids: [Int!], $name_pattern: String) {
+ deleteTemplates(templateids: $templateids, name_pattern: $name_pattern) {
+ id
+ message
+ }
+ }
+ `;
+ // Mock for query (to find ID for name_pattern deletion)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([{ templateid: "501", host: "BT_DEVICE_TRACKER" }]);
+ // Mock for delete
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ templateids: ["501"] });
+
+ const deleteResponse = await server.executeOperation({
+ query: deleteMutation,
+ variables: { name_pattern: "BT_DEVICE_TRACKER" },
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ expect(deleteResponse.body.kind).toBe('single');
+ // @ts-ignore
+ const deleteResult = deleteResponse.body.singleResult;
+ expect(deleteResult.errors).toBeUndefined();
+ expect(deleteResult.data.deleteTemplates).toHaveLength(1);
+ expect(deleteResult.data.deleteTemplates[0].message).toContain("deleted successfully");
+ });
+
+ test("Import and Export template groups comparison", async () => {
+ // 1. Import
+ const importSample = readFileSync(join(process.cwd(), 'docs', 'sample_import_template_groups_mutation.graphql'), 'utf-8').replace(/\r\n/g, '\n');
+ const importMutation = importSample.match(/```graphql\n([\s\S]*?)\n```/)![1];
+ const importVariables = JSON.parse(importSample.match(/```json\n([\s\S]*?)\n```/)![1]);
+
+ // Mock for import (8 groups in sample)
+ for (const group of importVariables.templateGroups) {
+ // Mock lookup (not found)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
+ // Mock creation
+ const mockGroupId = Math.floor(Math.random() * 1000).toString();
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ groupids: [mockGroupId] });
+ }
+
+ const importResponse = await server.executeOperation({
+ query: importMutation,
+ variables: importVariables,
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ expect(importResponse.body.kind).toBe('single');
+ // @ts-ignore
+ const importResult = importResponse.body.singleResult;
+ expect(importResult.errors).toBeUndefined();
+ expect(importResult.data.importTemplateGroups).toHaveLength(importVariables.templateGroups.length);
+
+ // 2. Export (Query)
+ const querySample = readFileSync(join(process.cwd(), 'docs', 'sample_all_template_groups_query.graphql'), 'utf-8').replace(/\r\n/g, '\n');
+ const query = querySample.match(/```graphql\n([\s\S]*?)\n```/)![1];
+ const queryVariables = JSON.parse(querySample.match(/```json\n([\s\S]*?)\n```/)![1]);
+
+ // Mock for query
+ const mockGroupsResponse = importVariables.templateGroups.map((g: any, index: number) => ({
+ groupid: (index + 1000).toString(),
+ name: g.groupName
+ }));
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockGroupsResponse);
+
+ const queryResponse = await server.executeOperation({
+ query: query,
+ variables: queryVariables,
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ expect(queryResponse.body.kind).toBe('single');
+ // @ts-ignore
+ const queryResult = queryResponse.body.singleResult;
+ expect(queryResult.errors).toBeUndefined();
+ expect(queryResult.data.allTemplateGroups).toHaveLength(importVariables.templateGroups.length);
+
+ // Verify names match
+ const importedNames = importVariables.templateGroups.map((g: any) => g.groupName).sort();
+ const exportedNames = queryResult.data.allTemplateGroups.map((g: any) => g.name).sort();
+ expect(exportedNames).toEqual(importedNames);
+
+ // 3. Delete Template Groups
+ const groupidsToDelete = queryResult.data.allTemplateGroups.map((g: any) => parseInt(g.groupid));
+ // Mock for query (for name_pattern)
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(queryResult.data.allTemplateGroups.map((g: any) => ({ groupid: g.groupid, name: g.name })));
+ // Mock for delete
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce({ groupids: groupidsToDelete.map((id: number) => id.toString()) });
+
+ const deleteMutation = `
+ mutation DeleteTemplateGroups($groupids: [Int!], $name_pattern: String) {
+ deleteTemplateGroups(groupids: $groupids, name_pattern: $name_pattern) {
+ id
+ message
+ error {
+ message
+ }
+ }
+ }
+ `;
+
+ const deleteResponse = await server.executeOperation({
+ query: deleteMutation,
+ variables: { name_pattern: "Templates/Roadwork/%" },
+ }, {
+ contextValue: { zabbixAuthToken: 'test-token' }
+ });
+
+ expect(deleteResponse.body.kind).toBe('single');
+ // @ts-ignore
+ const deleteResult = deleteResponse.body.singleResult;
+ expect(deleteResult.errors).toBeUndefined();
+ expect(deleteResult.data.deleteTemplateGroups).toHaveLength(groupidsToDelete.length);
+ expect(deleteResult.data.deleteTemplateGroups[0].message).toContain("deleted successfully");
+ });
+});
diff --git a/src/test/template_query.test.ts b/src/test/template_query.test.ts
new file mode 100644
index 0000000..c83761a
--- /dev/null
+++ b/src/test/template_query.test.ts
@@ -0,0 +1,112 @@
+
+import {createResolvers} from "../api/resolvers.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+import {QueryTemplatesArgs} from "../schema/generated/graphql.js";
+
+// Mocking ZabbixAPI
+jest.mock("../datasources/zabbix-api.js", () => ({
+ zabbixAPI: {
+ executeRequest: jest.fn(),
+ post: jest.fn(),
+ baseURL: "http://mock-zabbix"
+ },
+ ZABBIX_EDGE_DEVICE_BASE_GROUP: "Baustellen-Devices"
+}));
+
+describe("Template Resolver", () => {
+ let resolvers: any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resolvers = createResolvers();
+ });
+
+ test("templates query - returns all templates", async () => {
+ const mockTemplates = [
+ { templateid: "1", name: "Template 1", uuid: "uuid1" },
+ { templateid: "2", name: "Template 2", uuid: "uuid2" }
+ ];
+
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockTemplates);
+
+ const args: QueryTemplatesArgs = {};
+ const context = { zabbixAuthToken: "test-token" };
+
+ const result = await resolvers.Query.templates(null, args, context);
+
+ expect(result).toEqual(mockTemplates);
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.get", expect.objectContaining({
+ body: expect.objectContaining({
+ method: "template.get",
+ params: {}
+ })
+ }));
+ });
+
+ test("templates query - filters by hostids", async () => {
+ const mockTemplates = [{ templateid: "1", name: "Template 1" }];
+
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockTemplates);
+
+ const args: QueryTemplatesArgs = { hostids: [1] };
+ const context = { zabbixAuthToken: "test-token" };
+
+ const result = await resolvers.Query.templates(null, args, context);
+
+ expect(result).toEqual(mockTemplates);
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.get", expect.objectContaining({
+ body: expect.objectContaining({
+ method: "template.get",
+ params: expect.objectContaining({
+ templateids: [1]
+ })
+ })
+ }));
+ });
+
+ test("templates query - filters by name_pattern", async () => {
+ const mockTemplates = [{ templateid: "1", name: "Template 1" }];
+
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockTemplates);
+
+ const args: QueryTemplatesArgs = { name_pattern: "Template" };
+ const context = { zabbixAuthToken: "test-token" };
+
+ const result = await resolvers.Query.templates(null, args, context);
+
+ expect(result).toEqual(mockTemplates);
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.get", expect.objectContaining({
+ body: expect.objectContaining({
+ method: "template.get",
+ params: expect.objectContaining({
+ search: {
+ name: "Template"
+ }
+ })
+ })
+ }));
+ });
+
+ test("templates query - filters by name_pattern with % wildcard", async () => {
+ const mockTemplates = [{ templateid: "1", name: "Template 1" }];
+
+ (zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockTemplates);
+
+ const args: QueryTemplatesArgs = { name_pattern: "Temp%1" };
+ const context = { zabbixAuthToken: "test-token" };
+
+ const result = await resolvers.Query.templates(null, args, context);
+
+ expect(result).toEqual(mockTemplates);
+ expect(zabbixAPI.post).toHaveBeenCalledWith("template.get", expect.objectContaining({
+ body: expect.objectContaining({
+ method: "template.get",
+ params: expect.objectContaining({
+ search: {
+ name: "Temp%1"
+ }
+ })
+ })
+ }));
+ });
+});
diff --git a/src/testdata/templates/zbx_default_templates_vcr.yaml b/src/testdata/templates/zbx_default_templates_vcr.yaml
new file mode 100644
index 0000000..c25fa51
--- /dev/null
+++ b/src/testdata/templates/zbx_default_templates_vcr.yaml
@@ -0,0 +1,670 @@
+zabbix_export:
+ version: '7.4'
+ template_groups:
+ - uuid: 43aab460fe444f18886b19948413b7e3
+ name: Permissions/ConstructionSite
+ - uuid: 376524057e094c07aaa0cf7f524849dc
+ name: Templates/Roadwork/Controller
+ - uuid: 7d83c76454564390bb0e34600780eaec
+ name: Templates/Roadwork/Device-Capabilities
+ - uuid: 48d5d2a18a08448c96a931b63bb2c97d
+ name: Templates/Roadwork/Device-Capabilities/FLASH_ATTACHABLE
+ - uuid: 785986b84892468ea2e92d912747b1d3
+ name: Templates/Roadwork/Device-Capabilities/GEOLOCALIZABLE
+ - uuid: a4b79479e97a4b48972dcb476d45e55a
+ name: Templates/Roadwork/Device-Capabilities/HAS_OPERATIONAL_DATA
+ - uuid: 3604af8102644bee9dcaf0f9c1ee93a1
+ name: Templates/Roadwork/Devices
+ - uuid: 5ad0bd9e42a4487e869e9e41b38fe553
+ name: Templates/Roadwork/DisplayLibrary
+ templates:
+ - uuid: 27474f627cb344b782a81c16d7e0c7d1
+ template: BT_DEVICE_TRACKER
+ name: BT_DEVICE_TRACKER
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ templates:
+ - name: ROADWORK_DEVICE
+ groups:
+ - name: Templates/Roadwork/Devices
+ items:
+ - uuid: d4d3ec9f3ca940a39a721b6cfd2f3471
+ name: location
+ type: DEPENDENT
+ key: location
+ history: 2d
+ value_type: TEXT
+ preprocessing:
+ - type: JAVASCRIPT
+ parameters:
+ - |
+ var obj=JSON.parse(value);
+
+ if (obj["isFiltered"]) {
+ throw "Result is filtered";
+ return "filtered";
+ }
+
+ return value;
+ - type: NOT_MATCHES_REGEX
+ parameters:
+ - filtered
+ error_handler: DISCARD_VALUE
+ master_item:
+ key: 'mqtt.trap[deviceValue/location]'
+ - uuid: 380c4a7d752848cba3b5a59a0f9b13c0
+ name: MQTT_LOCATION
+ type: TRAP
+ key: 'mqtt.trap[deviceValue/location]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 29faf53c033840c0b1405f8240e30312
+ name: coords
+ type: DEPENDENT
+ key: state.current.values.coords
+ history: 2d
+ value_type: TEXT
+ preprocessing:
+ - type: JAVASCRIPT
+ parameters:
+ - |
+ var obj=JSON.parse(value);
+ var location = obj["location"];
+ var coords = location["coords"];
+ return JSON.stringify({
+ "btDeviceKey": obj["btDeviceKey"],
+ "timestamp": location["timestamp"],
+ "deviceName": obj["deviceName"],
+ "latitude": coords[1],
+ "longitude": coords[0],
+ "coords": coords
+ });
+ master_item:
+ key: location
+ tags:
+ - tag: hasValue
+ value: 'true'
+ - uuid: 1ae9486c18394e56b114c9cb4546deaf
+ name: geojson
+ type: DEPENDENT
+ key: state.current.values.geojson
+ history: 2d
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.location.setup
+ master_item:
+ key: location
+ tags:
+ - tag: hasValue
+ value: 'true'
+ tags:
+ - tag: class
+ value: roadwork
+ - tag: deviceType
+ value: bt_device_tracker_generic
+ - uuid: e6905dc6122944f3829ad28a9739e269
+ template: BT_TRACKER
+ name: BT_TRACKER
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ templates:
+ - name: ROADWORK_DEVICE
+ groups:
+ - name: Templates/Roadwork/Devices
+ items:
+ - uuid: b1e3062d67f94f7c8d064eff36a58b13
+ name: MQTT_STATE
+ type: DEPENDENT
+ key: currentstate
+ value_type: TEXT
+ preprocessing:
+ - type: JAVASCRIPT
+ parameters:
+ - |
+ var v = JSON.parse(value);
+ return JSON.stringify({
+ "count": v.count,
+ "timeFrom": v.timeFrom,
+ "timeUntil": v.timeUntil
+ });
+ master_item:
+ key: 'mqtt.trap[deviceValue/count]'
+ - uuid: 905c5f1b6e524bd2b227769a59f4df1b
+ name: MQTT_COUNT
+ type: TRAP
+ key: 'mqtt.trap[deviceValue/count]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 6fa441872c3140f4adecf39956245603
+ name: MQTT_DISTANCE
+ type: TRAP
+ key: 'mqtt.trap[deviceValue/distance]'
+ value_type: TEXT
+ - uuid: 69d2afa4a0324d818150e9473c3264f3
+ name: MQTT_NAME
+ type: TRAP
+ key: 'mqtt.trap[deviceValue/name]'
+ value_type: TEXT
+ - uuid: 45ff9430d27f47a492c98fce03fc7962
+ name: MQTT_SERVICE_DATA
+ type: TRAP
+ key: 'mqtt.trap[deviceValue/ServiceData]'
+ value_type: TEXT
+ - uuid: 3bf0d3017ea54e1da2a764c3f96bf97e
+ name: count
+ type: DEPENDENT
+ key: state.current.values.count
+ trends: '0'
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.count
+ master_item:
+ key: 'mqtt.trap[deviceValue/count]'
+ - uuid: f0d1fc72e2154613b349be86c6bdcfd6
+ name: timeFrom
+ type: DEPENDENT
+ key: state.current.values.timeFrom
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.timeFrom
+ - type: REGEX
+ parameters:
+ - 'T(\d\d:\d\d:\d\d):'
+ - \1
+ master_item:
+ key: 'mqtt.trap[deviceValue/count]'
+ - uuid: e55bf604808f4eb4a964ebeefdd9eb9e
+ name: timeUntil
+ type: DEPENDENT
+ key: state.current.values.timeUntil
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.timeUntil
+ - type: REGEX
+ parameters:
+ - 'T(\d\d:\d\d:\d\d):'
+ - \1
+ master_item:
+ key: 'mqtt.trap[deviceValue/count]'
+ tags:
+ - tag: class
+ value: roadwork
+ - tag: deviceType
+ value: bt_tracker_generic
+ - tag: deviceWidgetPreview.BOTTOM_LEFT.key
+ value: timeFrom
+ - tag: deviceWidgetPreview.BOTTOM_LEFT.unit
+ value: Startzeit
+ - tag: deviceWidgetPreview.BOTTOM_LEFT.unit_font_size
+ value: '8'
+ - tag: deviceWidgetPreview.BOTTOM_LEFT.value_font_size
+ value: '16'
+ - tag: deviceWidgetPreview.BOTTOM_RIGHT.key
+ value: timeUntil
+ - tag: deviceWidgetPreview.BOTTOM_RIGHT.unit
+ value: Endezeit
+ - tag: deviceWidgetPreview.BOTTOM_RIGHT.unit_font_size
+ value: '8'
+ - tag: deviceWidgetPreview.BOTTOM_RIGHT.value_font_size
+ value: '16'
+ - tag: deviceWidgetPreview.TOP_LEFT.key
+ value: count
+ - tag: deviceWidgetPreview.TOP_LEFT.unit
+ value: Geräte
+ - tag: deviceWidgetPreview.TOP_LEFT.unit_font_size
+ value: '8'
+ - tag: deviceWidgetPreview.TOP_LEFT.value_font_size
+ value: '24'
+ - uuid: 6490907a74964d0797c7acd1938bc553
+ template: GEOLOCATION
+ name: GEOLOCATION
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ groups:
+ - name: Templates/Roadwork/Device-Capabilities
+ - name: Templates/Roadwork/Device-Capabilities/GEOLOCALIZABLE
+ items:
+ - uuid: 4ad4d9a769744615816d190c34cb49c7
+ name: GPS_LOCATION_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/location]'
+ history: '0'
+ value_type: TEXT
+ description: 'old value: mqtt.get["rabbitmq","operationalValue/{$DEVICETYPE}/{HOST.HOST}/location","voltra_dev:voltradev","rabbit4voltra"]'
+ - uuid: 0e0012933b2345d4b119fdc50c526c73
+ name: GPS_LOCATION
+ type: DEPENDENT
+ key: state.operational.location.json
+ history: 90d
+ value_type: TEXT
+ master_item:
+ key: 'mqtt.trap[operationalValue/location]'
+ tags:
+ - tag: attributeName
+ value: location
+ - tag: hasValue
+ value: 'true'
+ - tag: topicType
+ value: operationalValue
+ - uuid: e9dcf0279afc4ed4a23e274df4c98356
+ name: LATITUDE
+ type: DEPENDENT
+ key: state.operational.location.latitude
+ history: 90d
+ value_type: FLOAT
+ inventory_link: LOCATION_LAT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.latitude
+ master_item:
+ key: state.operational.location.json
+ - uuid: 49d1677d3a4a4cfab23b2e8e50533833
+ name: LONGITUDE
+ type: DEPENDENT
+ key: state.operational.location.longitude
+ history: 90d
+ value_type: FLOAT
+ inventory_link: LOCATION_LON
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.longitude
+ master_item:
+ key: state.operational.location.json
+ - uuid: d7203eaab36749798014b6d3b1a43e24
+ name: LOCATION_NAME
+ type: DEPENDENT
+ key: state.operational.location.name
+ history: 90d
+ value_type: TEXT
+ inventory_link: LOCATION
+ preprocessing:
+ - type: JAVASCRIPT
+ parameters:
+ - |
+ var json = JSON.parse(value);
+ var lat = "lat=" + json.latitude;
+ var lon = "lon=" + json.longitude;
+
+ return lon + ", " + lat;
+ var geocoderURL = "https://photon.komoot.io/reverse?" + lon + "&" + lat;
+
+ var req = new HttpRequest();
+
+ req.addHeader('Content-Type: application/json');
+
+ resp = req.get(geocoderURL);
+
+ if (req.getStatus() != 200) {
+ throw 'Response code: '+req.getStatus();
+ }
+
+ var resultJson = JSON.parse(resp);
+
+ var features = resultJson.features;
+ if (features.length && features[0].properties) {
+ var props = features[0].properties;
+ return props.postcode + " " + props.city;
+ }
+ return value;
+ master_item:
+ key: state.operational.location.json
+ tags:
+ - tag: class
+ value: roadwork
+ macros:
+ - macro: '{$DEVICEKEY}'
+ value: '{HOST.HOST}'
+ - uuid: fe8bac9ac30f411cb5a322817760e71d
+ template: OPERATIONAL_DATA
+ name: OPERATIONAL_DATA
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ groups:
+ - name: Templates/Roadwork/Device-Capabilities
+ - name: Templates/Roadwork/Device-Capabilities/HAS_OPERATIONAL_DATA
+ items:
+ - uuid: 602290e9f42f4135b548e1cd45abe135
+ name: DENSITY_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/density]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 87e0a14266984247b81fdc757dea5bde
+ name: ERROR_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/error]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 644b0ec2e3d9448da1a69561ec10d19d
+ name: SIGNALSTRENGTH_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/signalstrength]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 67c01d7334a24823832bba74073cf356
+ name: TEMPERATURE_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/temperature]'
+ history: '0'
+ value_type: TEXT
+ tags:
+ - tag: attributeName
+ value: temperature
+ - tag: GRAPHQL_TYPE
+ value: Int
+ - tag: hasValue
+ value: 'true'
+ - tag: subscribeMqtt
+ value: 'true'
+ - tag: topicType
+ value: operationalValue
+ - uuid: 0352c80c749d4d91b386dab9c74ef3c6
+ name: VOLTAGE_MQTT
+ type: TRAP
+ key: 'mqtt.trap[operationalValue/voltage]'
+ history: '0'
+ value_type: TEXT
+ - uuid: 7aac8212c94044d28ada982c422f2bf7
+ name: DENSITY
+ type: DEPENDENT
+ key: state.operational.density
+ units: Kfz/Min
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.density
+ master_item:
+ key: 'mqtt.trap[operationalValue/density]'
+ tags:
+ - tag: attributeName
+ value: density
+ - tag: hasValue
+ value: 'true'
+ - tag: topicType
+ value: operationalValue
+ - uuid: 6c8c2f4cdc304b019d02026e7c3225ce
+ name: ERROR_HIGH
+ type: DEPENDENT
+ key: state.operational.errorHigh
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - '$[?(@.code>=1000)]'
+ error_handler: CUSTOM_VALUE
+ master_item:
+ key: state.operational.json_error
+ triggers:
+ - uuid: bc381851c3d84534866f3262828817a9
+ expression: 'last(/OPERATIONAL_DATA/state.operational.errorHigh)<>""'
+ name: DEVICE_ERROR_HIGH
+ event_name: DEVICE_ERROR_HIGH
+ priority: HIGH
+ manual_close: 'YES'
+ - uuid: 7634372683af42e7bb807bd2ee0e600a
+ name: ERROR_INFO
+ type: DEPENDENT
+ key: state.operational.errorInfo
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - '$[?(@.code=0)]'
+ error_handler: CUSTOM_VALUE
+ master_item:
+ key: state.operational.json_error
+ triggers:
+ - uuid: 201be3f4fd484651a0d2bb34586b408b
+ expression: 'last(/OPERATIONAL_DATA/state.operational.errorInfo)<>""'
+ name: DEVICE_ERROR_INFO
+ event_name: DEVICE_ERROR_INFO
+ priority: INFO
+ manual_close: 'YES'
+ - uuid: 58beaf63a07c44bd918f3f7c93be6d16
+ name: ERROR_WARNING
+ type: DEPENDENT
+ key: state.operational.errorWarning
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - '$[?(@.code>0&&@.code<1000)]'
+ error_handler: CUSTOM_VALUE
+ master_item:
+ key: state.operational.json_error
+ triggers:
+ - uuid: 208ef0feec0b469c8b3a8edc3ef12680
+ expression: 'last(/OPERATIONAL_DATA/state.operational.errorWarning)<>""'
+ name: DEVICE_ERROR_WARNING
+ event_name: DEVICE_ERROR_WARNING
+ priority: WARNING
+ manual_close: 'YES'
+ - uuid: cf7b08cec47a46ddb7ea110feab42c94
+ name: ERROR
+ type: DEPENDENT
+ key: state.operational.json_error
+ history: '0'
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.error
+ master_item:
+ key: 'mqtt.trap[operationalValue/error]'
+ tags:
+ - tag: attributeName
+ value: error
+ - tag: hasValue
+ value: 'true'
+ - tag: topicType
+ value: operationalValue
+ - uuid: a711e858297e4c80a61952b9848dd217
+ name: SIGNALSTRENGTH
+ type: DEPENDENT
+ key: state.operational.signalstrength
+ history: 90d
+ value_type: FLOAT
+ trends: '0'
+ units: dBm
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.signalstrength
+ master_item:
+ key: 'mqtt.trap[operationalValue/signalstrength]'
+ tags:
+ - tag: attributeName
+ value: signalstrength
+ - tag: hasValue
+ value: 'true'
+ triggers:
+ - uuid: 610d7f2cd5db4dc8938a61ffe81eb8e3
+ expression: 'nodata(/OPERATIONAL_DATA/state.operational.signalstrength,70s)=1'
+ name: NO_OPERATIONAL_VALUE
+ priority: WARNING
+ - uuid: 93df1a6ad1d640c883f446c85a220bd3
+ name: TEMPERATURE
+ type: DEPENDENT
+ key: state.operational.temperature
+ history: 90d
+ value_type: FLOAT
+ trends: '0'
+ units: °C
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.temperature
+ master_item:
+ key: 'mqtt.trap[operationalValue/temperature]'
+ tags:
+ - tag: attributeName
+ value: temperature
+ - tag: hasValue
+ value: 'true'
+ triggers:
+ - uuid: 8bc17b97d7fe4e4f9ac236e90d0b315d
+ expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)>60'
+ recovery_mode: RECOVERY_EXPRESSION
+ recovery_expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)<=60'
+ name: TEMPERATURE_HIGH
+ event_name: TEMPERATURE_HIGH
+ priority: HIGH
+ dependencies:
+ - name: TEMPERATURE_WARNING
+ expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)>50'
+ recovery_expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)<=50'
+ - uuid: 086f026b15e5410793d0604b4c7d51f6
+ expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)>50'
+ recovery_mode: RECOVERY_EXPRESSION
+ recovery_expression: 'last(/OPERATIONAL_DATA/state.operational.temperature,#3)<=50'
+ name: TEMPERATURE_WARNING
+ event_name: TEMPERATURE_WARNING
+ priority: AVERAGE
+ - uuid: cbc60c96e65d4111b902e3b133681067
+ name: TIMESTAMP
+ type: CALCULATED
+ key: state.operational.timestamp
+ value_type: TEXT
+ params: 'max(last(/{HOST.HOST}/state.operational.timestampSignalstrength),last(/{HOST.HOST}/state.operational.timestampVoltage))'
+ inventory_link: POC_2_NOTES
+ - uuid: ca1a1397f1aa458b88e531f699cacfeb
+ name: TIMESTAMP_SIGNALSTRENGTH
+ type: DEPENDENT
+ key: state.operational.timestampSignalstrength
+ history: '0'
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.timestamp
+ master_item:
+ key: 'mqtt.trap[operationalValue/signalstrength]'
+ tags:
+ - tag: attributeName
+ value: timestamp
+ - tag: hasValue
+ value: 'true'
+ - tag: subscribeMqtt
+ value: 'false'
+ - uuid: 919a09b55b304fc391bcd569f444b979
+ name: TIMESTAMP_VOLTAGE
+ type: DEPENDENT
+ key: state.operational.timestampVoltage
+ history: '0'
+ value_type: TEXT
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.timestamp
+ master_item:
+ key: 'mqtt.trap[operationalValue/voltage]'
+ tags:
+ - tag: attributeName
+ value: timestamp
+ - tag: hasValue
+ value: 'true'
+ - tag: subscribeMqtt
+ value: 'false'
+ - uuid: 3363ed4409b545b48ae7c6197a56aae2
+ name: VOLTAGE
+ type: DEPENDENT
+ key: state.operational.voltage
+ history: 90d
+ value_type: FLOAT
+ trends: '0'
+ units: V
+ preprocessing:
+ - type: JSONPATH
+ parameters:
+ - $.voltage
+ master_item:
+ key: 'mqtt.trap[operationalValue/voltage]'
+ tags:
+ - tag: attributeName
+ value: voltage
+ - tag: hasValue
+ value: 'true'
+ - tag: topicType
+ value: operationalValue
+ triggers:
+ - uuid: b411e1c0527a470184cac731c072fec2
+ expression: 'last(/OPERATIONAL_DATA/state.operational.voltage,#3)<=12'
+ recovery_mode: RECOVERY_EXPRESSION
+ recovery_expression: 'last(/OPERATIONAL_DATA/state.operational.voltage,#3)>=12'
+ name: VOLTAGE_LOW
+ event_name: VOLTAGE_LOW
+ priority: WARNING
+ description: |
+ warning if voltage < 24 of last 3 values
+ recovery if voltage >=24 of last 3 values
+ - uuid: f5c351920d544fc6abcf00fd9b26f51b
+ expression: 'last(/OPERATIONAL_DATA/state.operational.voltage,#3)<=11'
+ recovery_mode: RECOVERY_EXPRESSION
+ recovery_expression: 'last(/OPERATIONAL_DATA/state.operational.voltage,#3)>=11'
+ name: VOLTAGE_TOO_LOW
+ priority: DISASTER
+ tags:
+ - tag: class
+ value: roadwork
+ macros:
+ - macro: '{$DEVICEKEY}'
+ value: '{HOST.HOST}'
+ - uuid: 2b814a4751b745bcb08b5ee98f295dc9
+ template: ROADWORK_DEVICE
+ name: ROADWORK_DEVICE
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ groups:
+ - name: Templates/Roadwork/Devices
+ tags:
+ - tag: class
+ value: roadwork
+ - tag: hostType
+ value: Roadwork/Devices
+ - uuid: c53977cb7c6a45b18700502fd841db56
+ template: TEMPLATEGROUP_EXPORT_DUMMY_TEMPLATE
+ name: TEMPLATEGROUP_EXPORT_DUMMY_TEMPLATE
+ description: 'This template is linked to all template group which shall be part of the export templates - action because empty template groups will not be exported'
+ vendor:
+ name: 'Hilbig IT GmbH'
+ version: 2.1.1
+ groups:
+ - name: Permissions/Automatism
+ - name: Permissions/Automatism/Status
+ - name: Permissions/ConstructionSite
+ - name: Permissions/Library
+ - name: Permissions/Library/Item
+ - name: Templates/Roadwork/Controller
+ - name: Templates/Roadwork/Device-Capabilities
+ - name: Templates/Roadwork/Device-Capabilities/FLASH_ATTACHABLE
+ - name: Templates/Roadwork/Device-Capabilities/GEOLOCALIZABLE
+ - name: Templates/Roadwork/Device-Capabilities/HAS_OPERATIONAL_DATA
+ - name: Templates/Roadwork/Devices
+ - name: Templates/Roadwork/DisplayLibrary
+ tags:
+ - tag: class
+ value: roadwork
+ graphs:
+ - uuid: 2c8b16c6937a4d07912b6485ac8a1339
+ name: 'Mac address count'
+ graph_items:
+ - drawtype: GRADIENT_LINE
+ color: 1A7C11
+ calc_fnc: ALL
+ item:
+ host: BT_TRACKER
+ key: state.current.values.count