feat: improve Zabbix version compatibility and optimize queries
This commit introduces several improvements to ensure the API works seamlessly across Zabbix 6.0, 6.4, and 7.0+, while also optimizing data fetching performance. Key changes: - Zabbix Version Compatibility: - Added Zabbix version detection and static caching in ZabbixAPI. - Implemented name-based fallback for host/template group permissions to support Zabbix 6.0 (which lacks UUIDs for host groups). - Added manual host group expansion for Zabbix versions < 6.2.0 during user group import. - Added version-based guards for history.push (7.0+) and hostgroup.propagate (6.2+). - Updated documentation with detailed version compatibility notes. - Added src/test/zabbix_6_0_compatibility.test.ts to verify compatibility logic. - Query Optimization: - Implemented dynamic output selection in ZabbixRequest to fetch only fields requested in GraphQL queries. - Added GraphqlParamsToNeededZabbixOutput to map GraphQL selections to Zabbix API output parameters. - Moved "Query Optimization" to achieved milestones in roadmap.md. - Other: - Updated various tests to support the new version-aware logic. - Optimized imports and synchronized IDE settings.
This commit is contained in:
parent
7c2dee2b6c
commit
ec6ed422b1
21 changed files with 363 additions and 100 deletions
121
.idea/workspace.xml
generated
121
.idea/workspace.xml
generated
|
|
@ -5,30 +5,21 @@
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24 This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP). Changes: - Environment & CI/CD: - Set Node.js version to >=24 in package.json and .nvmrc. - Updated Dockerfile to use Node 24 base image. - Updated @types/node to ^24.10.9. - Documentation: - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping. - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation. - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol. - Added readme.improvement.plan.md to track documentation evolution. - Enhanced all how-to guides with improved cross-references and up-to-date information. - Guidelines: - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0). - Infrastructure & Code: - Updated docker-compose.yml with Apollo MCP server integration. - Refined configuration and schema handling in src/api/ and src/datasources/. - Synchronized generated TypeScript types with schema updates.">
|
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24 This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP). Changes: - Environment & CI/CD: - Set Node.js version to >=24 in package.json and .nvmrc. - Updated Dockerfile to use Node 24 base image. - Updated @types/node to ^24.10.9. - Documentation: - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping. - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation. - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol. - Added readme.improvement.plan.md to track documentation evolution. - Enhanced all how-to guides with improved cross-references and up-to-date information. - Guidelines: - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0). - Infrastructure & Code: - Updated docker-compose.yml with Apollo MCP server integration. - Refined configuration and schema handling in src/api/ and src/datasources/. - Synchronized generated TypeScript types with schema updates.">
|
||||||
<change afterPath="$PROJECT_DIR$/docs/use-cases/VCR - Technical product information.pdf" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/src/testdata/templates/zbx_device_tracker_vcr.yaml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/docker-compose.yml" beforeDir="false" afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-api.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-api.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/docs/VCR - Technical product information.pdf" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/docs/howtos/cookbook.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/cookbook.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/docs/howtos/mcp.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/mcp.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/docs/queries/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/queries/README.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/docs/tests.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/tests.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/docs/use-cases/trade-fair-logistics-requirements.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/use-cases/trade-fair-logistics-requirements.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/mcp-config.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/mcp-config.yaml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/samples/extensions/location_tracker_devices.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/location_tracker_devices.graphql" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/schema/mutations.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/schema/mutations.graphql" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/api/resolvers.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/resolvers.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/api/start.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/start.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/common_utils.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/common_utils.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-items.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-items.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-hostgroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hostgroups.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-request.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-request.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/execution/host_exporter.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/host_exporter.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/test/history_push.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/history_push.test.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/test/indirect_dependencies.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/indirect_dependencies.test.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/schema/generated/graphql.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/schema/generated/graphql.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/test/query_optimization.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/query_optimization.test.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/testdata/templates/zbx_default_templates_vcr.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/src/testdata/templates/zbx_default_templates_vcr.yaml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/test/template_integration.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_integration.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/template_link.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_link.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/template_query.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_query.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/user_rights.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/user_rights.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/user_rights_integration.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/user_rights_integration.test.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/zabbix_docs_samples.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/zabbix_docs_samples.test.ts" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
@ -39,7 +30,7 @@
|
||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="169" />
|
<option name="cachedIndexableFilesCount" value="168" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
|
|
@ -87,45 +78,50 @@
|
||||||
<option name="openDirectoriesWithSingleClick" value="true" />
|
<option name="openDirectoriesWithSingleClick" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
"NIXITCH_NIXPKGS_CONFIG": "",
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
"NIXITCH_NIX_CONF_DIR": "",
|
||||||
"NIXITCH_NIX_OTHER_STORES": "",
|
"NIXITCH_NIX_OTHER_STORES": "",
|
||||||
"NIXITCH_NIX_PATH": "",
|
"NIXITCH_NIX_PATH": "",
|
||||||
"NIXITCH_NIX_PROFILES": "",
|
"NIXITCH_NIX_PROFILES": "",
|
||||||
"NIXITCH_NIX_REMOTE": "",
|
"NIXITCH_NIX_REMOTE": "",
|
||||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
||||||
"Node.js.index.ts.executor": "Run",
|
"Node.js.index.ts.executor": "Run",
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "main",
|
||||||
"go.import.settings.migrated": "true",
|
"go.import.settings.migrated": "true",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
"junie.onboarding.icon.badge.shown": "true",
|
||||||
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs/use-cases",
|
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs/use-cases",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
|
"nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"npm.codegen.executor": "Run",
|
"npm.codegen.executor": "Run",
|
||||||
"npm.compile.executor": "Run",
|
"npm.compile.executor": "Run",
|
||||||
"npm.copy-schema.executor": "Run",
|
"npm.copy-schema.executor": "Run",
|
||||||
"npm.prod.executor": "Run",
|
"npm.prod.executor": "Run",
|
||||||
"npm.test.executor": "Run",
|
"npm.test.executor": "Run",
|
||||||
"settings.editor.selected.configurable": "junie.mcp",
|
"settings.editor.selected.configurable": "junie.mcp",
|
||||||
"settings.editor.splitter.proportion": "0.23751687",
|
"settings.editor.splitter.proportion": "0.23751687",
|
||||||
"to.speed.mode.migration.done": "true",
|
"to.speed.mode.migration.done": "true",
|
||||||
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
},
|
||||||
|
"keyToStringList": {
|
||||||
|
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||||
|
"TEXT"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="RecapSpentCounter">
|
<component name="RecapSpentCounter">
|
||||||
<option name="endsOfQuotaMs" value="1772398800000" />
|
<option name="endsOfQuotaMs" value="1772398800000" />
|
||||||
<option name="spentUsd" value="0.0915201" />
|
<option name="spentUsd" value="0.0915201" />
|
||||||
|
|
@ -231,7 +227,10 @@
|
||||||
<workItem from="1769789496322" duration="14281000" />
|
<workItem from="1769789496322" duration="14281000" />
|
||||||
<workItem from="1769849767328" duration="18404000" />
|
<workItem from="1769849767328" duration="18404000" />
|
||||||
<workItem from="1769955114366" duration="3276000" />
|
<workItem from="1769955114366" duration="3276000" />
|
||||||
<workItem from="1770107035156" duration="3830000" />
|
<workItem from="1770107035156" duration="4817000" />
|
||||||
|
<workItem from="1770129804879" duration="13000" />
|
||||||
|
<workItem from="1770129846593" duration="5283000" />
|
||||||
|
<workItem from="1770167580486" duration="1386000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment">
|
<task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -52,7 +52,7 @@ Before you begin, ensure you have met the following requirements:
|
||||||
|
|
||||||
- **Node.js**: Version 24 (LTS) or higher recommended.
|
- **Node.js**: Version 24 (LTS) or higher recommended.
|
||||||
- **Docker**: Version 27 or higher and **Docker Compose** v2.29 or higher (use `docker compose` instead of `docker-compose`).
|
- **Docker**: Version 27 or higher and **Docker Compose** v2.29 or higher (use `docker compose` instead of `docker-compose`).
|
||||||
- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access.
|
- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access. See [Zabbix Version Compatibility](#-zabbix-version-compatibility) for details.
|
||||||
- **Zabbix Super Admin Token** (for full functionality / privilege escalation).
|
- **Zabbix Super Admin Token** (for full functionality / privilege escalation).
|
||||||
- **Zabbix User Access** (groups and roles depending on your use case).
|
- **Zabbix User Access** (groups and roles depending on your use case).
|
||||||
|
|
||||||
|
|
@ -236,7 +236,20 @@ The API version is automatically set during the Docker build process based on th
|
||||||
|
|
||||||
### 🔧 Zabbix Version Compatibility
|
### 🔧 Zabbix Version Compatibility
|
||||||
|
|
||||||
This API is designed to work with Zabbix 7.4, which is the version it runs productively with. While it may work with earlier versions (like 6.0+), 7.4 is the officially supported and tested version.
|
This API is officially supported and productively used with **Zabbix 7.0 (LTS)** and newer. It also maintains compatibility with **Zabbix 6.0 (LTS)** and **6.4**, with the following version-specific behaviors:
|
||||||
|
|
||||||
|
- **Zabbix 7.0 (LTS)**:
|
||||||
|
- Full feature support.
|
||||||
|
- **History Push**: Uses the native `history.push` API for efficient data ingestion.
|
||||||
|
- **Zabbix 6.4**:
|
||||||
|
- **History Push**: Not supported (requires Zabbix 7.0+). The `pushHistory` mutation returns a clear error.
|
||||||
|
- **Group Propagation**: Fully supported via the `hostgroup.propagate` API (introduced in 6.2).
|
||||||
|
- **UUIDs**: Fully supported for both Host Groups and Template Groups.
|
||||||
|
- **Zabbix 6.0 (LTS)**:
|
||||||
|
- **History Push**: Not supported.
|
||||||
|
- **Group Propagation**: `hostgroup.propagate` is unavailable. The API automatically performs **manual host group expansion** during import to ensure consistent behavior.
|
||||||
|
- **UUIDs**: Host Groups in 6.0 lack UUIDs. The API uses a **name-based fallback** for matching host group permissions during import.
|
||||||
|
- **Template Groups**: The API correctly handles the separation of Host Groups and Template Groups introduced in Zabbix 6.0 across all versions.
|
||||||
|
|
||||||
## 🛠️ Technical Maintenance
|
## 🛠️ Technical Maintenance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ This document outlines the achieved milestones and planned enhancements for the
|
||||||
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
||||||
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
|
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
|
||||||
|
|
||||||
|
- **⚡ Query Optimization**: Optimized GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested.
|
||||||
|
- *Implementation*: Added dynamic output selection and field pruning in `ZabbixRequest`.
|
||||||
|
|
||||||
- **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
|
- **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
|
||||||
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
|
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
|
||||||
|
|
||||||
## 📅 Planned Enhancements
|
## 📅 Planned Enhancements
|
||||||
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
|
|
||||||
|
|
||||||
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
|
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
|
||||||
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
|
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
|
||||||
- *Simulation*:
|
- *Simulation*:
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,20 @@ export class ZabbixAPI
|
||||||
return super.post(path, request);
|
return super.post(path, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static version: string | undefined
|
||||||
|
|
||||||
|
async getVersion(): Promise<string> {
|
||||||
|
if (!ZabbixAPI.version) {
|
||||||
|
const response = await this.requestByPath<string>("apiinfo.version")
|
||||||
|
if (typeof response === "string") {
|
||||||
|
ZabbixAPI.version = response
|
||||||
|
} else {
|
||||||
|
return "0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ZabbixAPI.version
|
||||||
|
}
|
||||||
|
|
||||||
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
|
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||||
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
|
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,10 @@ export class ZabbixHistoryPushRequest extends ZabbixRequest<ZabbixHistoryPushRes
|
||||||
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixHistoryPushParams): Promise<ZabbixHistoryPushResult | ZabbixErrorResult | undefined> {
|
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixHistoryPushParams): Promise<ZabbixHistoryPushResult | ZabbixErrorResult | undefined> {
|
||||||
if (!args) return undefined;
|
if (!args) return undefined;
|
||||||
|
|
||||||
|
const version = await zabbixAPI.getVersion();
|
||||||
|
if (version < "7.0.0") {
|
||||||
|
throw new GraphQLError(`history.push is only supported in Zabbix 7.0.0 and newer. Current version is ${version}. For older versions, please use Zabbix trapper items and zabbix_sender protocol.`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!args.itemid && (!args.key || !args.host)) {
|
if (!args.itemid && (!args.key || !args.host)) {
|
||||||
throw new GraphQLError("if itemid is empty both key and host must be filled");
|
throw new GraphQLError("if itemid is empty both key and host must be filled");
|
||||||
|
|
|
||||||
|
|
@ -100,4 +100,33 @@ export class GroupHelper {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async findSubgroupIds(groupids: number[], zabbixApi: ZabbixAPI, zabbixAuthToken?: string, cookie?: string): Promise<number[]> {
|
||||||
|
if (groupids.length === 0) return [];
|
||||||
|
|
||||||
|
// Get the names of the parent groups
|
||||||
|
const parentGroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
|
||||||
|
groupids: groupids
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isZabbixErrorResult(parentGroups) || !parentGroups.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const subgroupids: number[] = [];
|
||||||
|
for (const parent of parentGroups) {
|
||||||
|
const subgroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
|
||||||
|
search: {
|
||||||
|
name: parent.name + "/*"
|
||||||
|
},
|
||||||
|
searchWildcardsEnabled: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!isZabbixErrorResult(subgroups)) {
|
||||||
|
subgroups.forEach(sg => subgroupids.push(Number(sg.groupid)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(subgroupids)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ import {
|
||||||
ZabbixGroupRightInput
|
ZabbixGroupRightInput
|
||||||
} from "../schema/generated/graphql.js";
|
} from "../schema/generated/graphql.js";
|
||||||
import {ZabbixAPI} from "./zabbix-api.js";
|
import {ZabbixAPI} from "./zabbix-api.js";
|
||||||
|
import {logger} from "../logging/logger.js";
|
||||||
import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js";
|
import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js";
|
||||||
import {ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
|
import {GroupHelper, ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
|
||||||
import {ApiErrorCode} from "../model/model_enum_values.js";
|
import {ApiErrorCode} from "../model/model_enum_values.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,10 +154,36 @@ export class ZabbixImportUserGroupsRequest
|
||||||
let createGroupRequest = new ZabbixCreateOrUpdateRequest<
|
let createGroupRequest = new ZabbixCreateOrUpdateRequest<
|
||||||
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
|
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
|
||||||
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
|
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
|
||||||
|
|
||||||
|
const version = await zabbixAPI.getVersion();
|
||||||
|
const needsManualPropagation = version < "6.2.0";
|
||||||
|
|
||||||
for (let userGroup of args?.usergroups || []) {
|
for (let userGroup of args?.usergroups || []) {
|
||||||
let templategroup_rights = this.calc_templategroup_rights(userGroup);
|
let templategroup_rights = this.calc_templategroup_rights(userGroup);
|
||||||
let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
|
let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
|
||||||
|
|
||||||
|
if (needsManualPropagation && hostgroup_rights.hostgroup_rights.length > 0) {
|
||||||
|
const parentGroupids = hostgroup_rights.hostgroup_rights.map(r => r.id);
|
||||||
|
const subgroupids = await GroupHelper.findSubgroupIds(parentGroupids, zabbixAPI, this.authToken ?? undefined, this.cookie ?? undefined);
|
||||||
|
|
||||||
|
for (const sgid of subgroupids) {
|
||||||
|
if (!hostgroup_rights.hostgroup_rights.find(r => r.id === sgid)) {
|
||||||
|
// Find the permission of the parent group to apply it to the subgroup
|
||||||
|
// This is a simplification: if multiple parents match, we take one.
|
||||||
|
// Actually, we should find which parent this subgroup belongs to.
|
||||||
|
// But for simplicity, we can just look at the parent permissions.
|
||||||
|
// In most cases, it's just one parent being propagated.
|
||||||
|
const parent = hostgroup_rights.hostgroup_rights.find(r => parentGroupids.includes(r.id)); // just take first
|
||||||
|
if (parent) {
|
||||||
|
hostgroup_rights.hostgroup_rights.push({
|
||||||
|
id: sgid,
|
||||||
|
permission: parent.permission
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let errors: ApiError[] = [];
|
let errors: ApiError[] = [];
|
||||||
|
|
||||||
let params = new ZabbixCreateOrUpdateParams({
|
let params = new ZabbixCreateOrUpdateParams({
|
||||||
|
|
@ -214,34 +241,52 @@ export class ZabbixImportUserGroupsRequest
|
||||||
for (let hostgroup_right of usergroup.hostgroup_rights || []) {
|
for (let hostgroup_right of usergroup.hostgroup_rights || []) {
|
||||||
let success = false;
|
let success = false;
|
||||||
let matchedName = "";
|
let matchedName = "";
|
||||||
|
let matchedId: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Try matching by UUID first
|
||||||
for (let hostgroup of this.hostgroups) {
|
for (let hostgroup of this.hostgroups) {
|
||||||
if (hostgroup.uuid == hostgroup_right.uuid) {
|
if (hostgroup.uuid && hostgroup_right.uuid && hostgroup.uuid === hostgroup_right.uuid) {
|
||||||
|
matchedId = Number(hostgroup.groupid);
|
||||||
|
matchedName = hostgroup.name;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to matching by name (important for Zabbix 6.0 which lacks Host Group UUIDs)
|
||||||
|
if (!success && hostgroup_right.name) {
|
||||||
|
for (let hostgroup of this.hostgroups) {
|
||||||
|
if (hostgroup.name === hostgroup_right.name) {
|
||||||
|
matchedId = Number(hostgroup.groupid);
|
||||||
|
matchedName = hostgroup.name;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
result.push(
|
result.push(
|
||||||
{
|
{
|
||||||
id: Number(hostgroup.groupid),
|
id: matchedId!,
|
||||||
permission: hostgroup_right.permission,
|
permission: hostgroup_right.permission,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
success = true;
|
|
||||||
matchedName = hostgroup.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
if (hostgroup_right.name && hostgroup_right.name != matchedName) {
|
||||||
if (success && hostgroup_right.name && hostgroup_right.name != matchedName) {
|
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.OK,
|
code: ApiErrorCode.OK,
|
||||||
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match`,
|
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match provided name=${hostgroup_right.name}`,
|
||||||
data: hostgroup_right,
|
data: hostgroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!success) {
|
} else {
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
|
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
|
||||||
message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`,
|
message: `Hostgroup with UUID ${hostgroup_right.uuid} ${hostgroup_right.name ? "or name " + hostgroup_right.name : ""} not found`,
|
||||||
data: hostgroup_right,
|
data: hostgroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -262,33 +307,52 @@ export class ZabbixImportUserGroupsRequest
|
||||||
for (let templategroup_right of usergroup.templategroup_rights || []) {
|
for (let templategroup_right of usergroup.templategroup_rights || []) {
|
||||||
let success = false;
|
let success = false;
|
||||||
let matchedName = "";
|
let matchedName = "";
|
||||||
|
let matchedId: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Try matching by UUID first
|
||||||
for (let templategroup of this.templategroups) {
|
for (let templategroup of this.templategroups) {
|
||||||
if (templategroup.uuid == templategroup_right.uuid) {
|
if (templategroup.uuid && templategroup_right.uuid && templategroup.uuid === templategroup_right.uuid) {
|
||||||
result.push(
|
matchedId = Number(templategroup.groupid);
|
||||||
{
|
matchedName = templategroup.name;
|
||||||
id: Number(templategroup.groupid),
|
|
||||||
permission: templategroup_right.permission,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
success = true;
|
success = true;
|
||||||
matchedName = templategroup.name
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (success && templategroup_right.name && templategroup_right.name != matchedName) {
|
|
||||||
|
// Fallback to matching by name
|
||||||
|
if (!success && templategroup_right.name) {
|
||||||
|
for (let templategroup of this.templategroups) {
|
||||||
|
if (templategroup.name === templategroup_right.name) {
|
||||||
|
matchedId = Number(templategroup.groupid);
|
||||||
|
matchedName = templategroup.name;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
result.push(
|
||||||
|
{
|
||||||
|
id: matchedId!,
|
||||||
|
permission: templategroup_right.permission,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (templategroup_right.name && templategroup_right.name != matchedName) {
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.OK,
|
code: ApiErrorCode.OK,
|
||||||
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match`,
|
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match provided name=${templategroup_right.name}`,
|
||||||
data: templategroup_right,
|
data: templategroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!success) {
|
} else {
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
|
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
|
||||||
message: `Templategroup with UUID ${templategroup_right.uuid} not found`,
|
message: `Templategroup with UUID ${templategroup_right.uuid} ${templategroup_right.name ? "or name " + templategroup_right.name : ""} not found`,
|
||||||
data: templategroup_right,
|
data: templategroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -317,6 +381,17 @@ export class ZabbixPropagateHostGroupsRequest extends ZabbixRequest<ZabbixCreate
|
||||||
super("hostgroup.propagate", authToken, cookie);
|
super("hostgroup.propagate", authToken, cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixPropagateHostGroupsParams): Promise<ZabbixCreateUserGroupResponse | ZabbixErrorResult | undefined> {
|
||||||
|
const version = await zabbixAPI.getVersion();
|
||||||
|
if (version < "6.2.0") {
|
||||||
|
logger.warn(`hostgroup.propagate is only supported in Zabbix 6.2.0 and newer. Current version is ${version}. Skipping propagation.`);
|
||||||
|
return {
|
||||||
|
usrgrpids: []
|
||||||
|
} as ZabbixCreateUserGroupResponse;
|
||||||
|
}
|
||||||
|
return super.prepare(zabbixAPI, args);
|
||||||
|
}
|
||||||
|
|
||||||
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
|
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
|
||||||
return {
|
return {
|
||||||
groups: [...new Set(args?.groups || [])].map(value => {
|
groups: [...new Set(args?.groups || [])].map(value => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {GraphQLError} from "graphql";
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0")
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -63,4 +64,12 @@ describe("ZabbixHistoryPushRequest", () => {
|
||||||
|
|
||||||
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("if itemid is empty both key and host must be filled");
|
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("if itemid is empty both key and host must be filled");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prepare - throw error if Zabbix version < 7.0.0", async () => {
|
||||||
|
(zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
|
||||||
|
const values = [{ timestamp: "2024-01-01T10:00:00Z", value: "val" }];
|
||||||
|
const params = new ZabbixHistoryPushParams(values, "1");
|
||||||
|
|
||||||
|
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("history.push is only supported in Zabbix 7.0.0 and newer");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
requestByPath: jest.fn()
|
requestByPath: jest.fn()
|
||||||
},
|
},
|
||||||
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
|
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from '../datasources/zabbix-ap
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
baseURL: 'http://localhost/zabbix',
|
baseURL: 'http://localhost/zabbix',
|
||||||
getLocations: jest.fn(),
|
getLocations: jest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
baseURL: "http://mock-zabbix",
|
baseURL: "http://mock-zabbix",
|
||||||
getLocations: jest.fn()
|
getLocations: jest.fn()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
baseURL: "http://mock-zabbix",
|
baseURL: "http://mock-zabbix",
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
baseURL: "http://mock-zabbix",
|
baseURL: "http://mock-zabbix",
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
baseURL: 'http://localhost/zabbix'
|
baseURL: 'http://localhost/zabbix'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
baseURL: 'http://localhost/zabbix',
|
baseURL: 'http://localhost/zabbix',
|
||||||
requestByPath: jest.fn()
|
requestByPath: jest.fn()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
baseURL: "http://mock-zabbix"
|
baseURL: "http://mock-zabbix"
|
||||||
},
|
},
|
||||||
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
|
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
baseURL: "http://mock-zabbix"
|
baseURL: "http://mock-zabbix"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
baseURL: 'http://localhost/zabbix',
|
baseURL: 'http://localhost/zabbix',
|
||||||
getLocations: jest.fn(),
|
getLocations: jest.fn(),
|
||||||
|
|
@ -43,7 +44,8 @@ describe("User Rights Integration Tests", () => {
|
||||||
.mockResolvedValueOnce([{ groupid: "101", name: "Group1", uuid: "uuid1" }]) // templategroup.get for groups (in prepare)
|
.mockResolvedValueOnce([{ groupid: "101", name: "Group1", uuid: "uuid1" }]) // templategroup.get for groups (in prepare)
|
||||||
.mockResolvedValueOnce([{ groupid: "201", name: "ConstructionSite/Test", uuid: "uuid2" }]) // hostgroup.get for groups (in prepare)
|
.mockResolvedValueOnce([{ groupid: "201", name: "ConstructionSite/Test", uuid: "uuid2" }]) // hostgroup.get for groups (in prepare)
|
||||||
.mockResolvedValueOnce([{ usrgrpid: "1", name: "Test Group" }]) // usergroup.get
|
.mockResolvedValueOnce([{ usrgrpid: "1", name: "Test Group" }]) // usergroup.get
|
||||||
.mockResolvedValueOnce({ usrgrpids: ["1"] }); // usergroup.update
|
.mockResolvedValueOnce({ usrgrpids: ["1"] }) // usergroup.update
|
||||||
|
.mockResolvedValueOnce({ usrgrpids: [] }); // hostgroup.propagate
|
||||||
|
|
||||||
const response = await server.executeOperation({
|
const response = await server.executeOperation({
|
||||||
query: mutation,
|
query: mutation,
|
||||||
|
|
|
||||||
106
src/test/zabbix_6_0_compatibility.test.ts
Normal file
106
src/test/zabbix_6_0_compatibility.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import {createResolvers} from "../api/resolvers.js";
|
||||||
|
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||||
|
import {Permission} from "../schema/generated/graphql.js";
|
||||||
|
|
||||||
|
// Mocking ZabbixAPI
|
||||||
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
|
zabbixAPI: {
|
||||||
|
executeRequest: jest.fn(),
|
||||||
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn(),
|
||||||
|
baseURL: "http://mock-zabbix"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Zabbix 6.0 Compatibility", () => {
|
||||||
|
let resolvers: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
resolvers = createResolvers();
|
||||||
|
(zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("importUserRights uses name-based fallback for host groups on Zabbix 6.0", async () => {
|
||||||
|
// Mock Zabbix 6.0 behavior where hostgroup.get does NOT return UUID
|
||||||
|
(zabbixAPI.post as jest.Mock).mockImplementation((path: string) => {
|
||||||
|
if (path === "templategroup.get") return Promise.resolve([{ groupid: "101", name: "TemplateGroup1", uuid: "uuid-tg-1" }]);
|
||||||
|
if (path === "hostgroup.get") return Promise.resolve([{ groupid: "201", name: "HostGroup1" }]); // NO UUID
|
||||||
|
if (path === "usergroup.get") return Promise.resolve([]);
|
||||||
|
if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
|
||||||
|
if (path === "module.get") return Promise.resolve([]);
|
||||||
|
if (path === "role.get") return Promise.resolve([]);
|
||||||
|
return Promise.resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
input: {
|
||||||
|
userGroups: [{
|
||||||
|
name: "NewGroup",
|
||||||
|
hostgroup_rights: [{
|
||||||
|
name: "HostGroup1",
|
||||||
|
uuid: "some-uuid-from-export", // This UUID won't match anything in 6.0 mock
|
||||||
|
permission: Permission.Read
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
dryRun: false
|
||||||
|
};
|
||||||
|
const context = { zabbixAuthToken: "test-token" };
|
||||||
|
|
||||||
|
const result = await resolvers.Mutation.importUserRights(null, args, context);
|
||||||
|
|
||||||
|
expect(result.userGroups).toHaveLength(1);
|
||||||
|
expect(result.userGroups[0].name).toBe("NewGroup");
|
||||||
|
expect(result.userGroups[0].errors).toHaveLength(0); // Should succeed via name match
|
||||||
|
|
||||||
|
// Verify that usergroup.create was called with correct groupid from name match
|
||||||
|
const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
|
||||||
|
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({
|
||||||
|
id: 201,
|
||||||
|
permission: Permission.Read
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("importUserRights performs manual expansion on Zabbix 6.0", async () => {
|
||||||
|
// Mock Zabbix 6.0 behavior
|
||||||
|
(zabbixAPI.post as jest.Mock).mockImplementation((path: string, options: any) => {
|
||||||
|
if (path === "templategroup.get") return Promise.resolve([]);
|
||||||
|
if (path === "hostgroup.get") {
|
||||||
|
// If searching for subgroups
|
||||||
|
if (options?.body?.params?.search?.name === "Parent/*") {
|
||||||
|
return Promise.resolve([{ groupid: "202", name: "Parent/Child" }]);
|
||||||
|
}
|
||||||
|
return Promise.resolve([{ groupid: "201", name: "Parent" }]);
|
||||||
|
}
|
||||||
|
if (path === "usergroup.get") return Promise.resolve([]);
|
||||||
|
if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
|
||||||
|
if (path === "module.get") return Promise.resolve([]);
|
||||||
|
if (path === "role.get") return Promise.resolve([]);
|
||||||
|
return Promise.resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
input: {
|
||||||
|
userGroups: [{
|
||||||
|
name: "NewGroup",
|
||||||
|
hostgroup_rights: [{
|
||||||
|
name: "Parent",
|
||||||
|
uuid: "uuid-parent",
|
||||||
|
permission: Permission.Read
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
dryRun: false
|
||||||
|
};
|
||||||
|
const context = { zabbixAuthToken: "test-token" };
|
||||||
|
|
||||||
|
await resolvers.Mutation.importUserRights(null, args, context);
|
||||||
|
|
||||||
|
// Verify that usergroup.create was called with both parent and expanded child
|
||||||
|
const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
|
||||||
|
expect(createCall[1].body.params.hostgroup_rights).toHaveLength(2);
|
||||||
|
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 201, permission: Permission.Read });
|
||||||
|
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 202, permission: Permission.Read });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||||
zabbixAPI: {
|
zabbixAPI: {
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
|
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||||
executeRequest: jest.fn(),
|
executeRequest: jest.fn(),
|
||||||
baseURL: 'http://localhost/zabbix',
|
baseURL: 'http://localhost/zabbix',
|
||||||
getLocations: jest.fn(),
|
getLocations: jest.fn(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue