Compare commits
No commits in common. "14a0df4c183e9f4f6c43fcbd55a025ddc1ed1300" and "7c2dee2b6c30aa63c4589692f1ac8657db59b006" have entirely different histories.
14a0df4c18
...
7c2dee2b6c
36 changed files with 210 additions and 759 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -74,7 +74,10 @@ web_modules/
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
|
|
|
||||||
121
.idea/workspace.xml
generated
121
.idea/workspace.xml
generated
|
|
@ -5,21 +5,30 @@
|
||||||
</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$/src/datasources/zabbix-api.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-api.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/docker-compose.yml" beforeDir="false" afterPath="$PROJECT_DIR$/docker-compose.yml" 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-hostgroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hostgroups.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-usergroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.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/test/history_push.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/history_push.test.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/indirect_dependencies.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/indirect_dependencies.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/query_optimization.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/query_optimization.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/template_integration.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_integration.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_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" />
|
||||||
|
|
@ -30,7 +39,7 @@
|
||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="168" />
|
<option name="cachedIndexableFilesCount" value="169" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
|
|
@ -78,50 +87,45 @@
|
||||||
<option name="openDirectoriesWithSingleClick" value="true" />
|
<option name="openDirectoriesWithSingleClick" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
"NIXITCH_NIXPKGS_CONFIG": "",
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
"NIXITCH_NIX_CONF_DIR": "",
|
||||||
"NIXITCH_NIX_OTHER_STORES": "",
|
"NIXITCH_NIX_OTHER_STORES": "",
|
||||||
"NIXITCH_NIX_PATH": "",
|
"NIXITCH_NIX_PATH": "",
|
||||||
"NIXITCH_NIX_PROFILES": "",
|
"NIXITCH_NIX_PROFILES": "",
|
||||||
"NIXITCH_NIX_REMOTE": "",
|
"NIXITCH_NIX_REMOTE": "",
|
||||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
||||||
"Node.js.index.ts.executor": "Run",
|
"Node.js.index.ts.executor": "Run",
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "main",
|
||||||
"go.import.settings.migrated": "true",
|
"go.import.settings.migrated": "true",
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
"junie.onboarding.icon.badge.shown": "true",
|
||||||
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/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" />
|
||||||
|
|
@ -227,10 +231,7 @@
|
||||||
<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="4817000" />
|
<workItem from="1770107035156" duration="3830000" />
|
||||||
<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" />
|
||||||
|
|
|
||||||
31
README.md
31
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. See [Zabbix Version Compatibility](#-zabbix-version-compatibility) for details.
|
- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access.
|
||||||
- **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).
|
||||||
|
|
||||||
|
|
@ -81,20 +81,6 @@ Builds the project and runs the compiled code:
|
||||||
npm run prod
|
npm run prod
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🐳 Local Zabbix Environment
|
|
||||||
For development and testing, you can start a complete environment including Zabbix from scratch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start with local Zabbix (latest 7.0)
|
|
||||||
docker compose --profile zabbix-local up -d
|
|
||||||
|
|
||||||
# Start with a specific Zabbix version (e.g. 6.0)
|
|
||||||
ZABBIX_VERSION=alpine-6.0-latest docker compose --profile zabbix-local up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Guide**: For detailed setup and configuration of the local environment, see [Local Development Environment](./docs/howtos/local_development.md).
|
|
||||||
> **Important**: On fresh Zabbix installations, you must manually create the base host group (e.g., `Roadwork`) before the API can import devices.
|
|
||||||
|
|
||||||
The API will be available at `http://localhost:4000/`.
|
The API will be available at `http://localhost:4000/`.
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
@ -250,20 +236,7 @@ The API version is automatically set during the Docker build process based on th
|
||||||
|
|
||||||
### 🔧 Zabbix Version Compatibility
|
### 🔧 Zabbix Version Compatibility
|
||||||
|
|
||||||
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:
|
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.
|
||||||
|
|
||||||
- **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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,50 +43,5 @@ services:
|
||||||
- mcp-shared:/mcp-data
|
- mcp-shared:/mcp-data
|
||||||
command: sh -c "cat /schema/*.graphql > /mcp-data/schema.graphql"
|
command: sh -c "cat /schema/*.graphql > /mcp-data/schema.graphql"
|
||||||
|
|
||||||
postgres-server:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
profiles:
|
|
||||||
- zabbix-local
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=zabbix
|
|
||||||
- POSTGRES_PASSWORD=zabbix
|
|
||||||
- POSTGRES_DB=zabbix
|
|
||||||
volumes:
|
|
||||||
- zbx_db_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
zabbix-server:
|
|
||||||
image: zabbix/zabbix-server-pgsql:${ZABBIX_VERSION:-alpine-7.0-latest}
|
|
||||||
profiles:
|
|
||||||
- zabbix-local
|
|
||||||
ports:
|
|
||||||
- "10051:10051"
|
|
||||||
environment:
|
|
||||||
- DB_SERVER_HOST=postgres-server
|
|
||||||
- POSTGRES_USER=zabbix
|
|
||||||
- POSTGRES_PASSWORD=zabbix
|
|
||||||
- POSTGRES_DB=zabbix
|
|
||||||
- ZBX_ALLOWUNSUPPORTEDDBVERSIONS=1
|
|
||||||
depends_on:
|
|
||||||
- postgres-server
|
|
||||||
|
|
||||||
zabbix-web:
|
|
||||||
image: zabbix/zabbix-web-nginx-pgsql:${ZABBIX_VERSION:-alpine-7.0-latest}
|
|
||||||
profiles:
|
|
||||||
- zabbix-local
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
- ZBX_SERVER_HOST=zabbix-server
|
|
||||||
- DB_SERVER_HOST=postgres-server
|
|
||||||
- POSTGRES_USER=zabbix
|
|
||||||
- POSTGRES_PASSWORD=zabbix
|
|
||||||
- POSTGRES_DB=zabbix
|
|
||||||
- PHP_TZ=UTC
|
|
||||||
- ZBX_ALLOWUNSUPPORTEDDBVERSIONS=1
|
|
||||||
depends_on:
|
|
||||||
- postgres-server
|
|
||||||
- zabbix-server
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mcp-shared:
|
mcp-shared:
|
||||||
zbx_db_data:
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,6 @@ Discover how the permission system works, how to define permission levels using
|
||||||
### 🛠️ [Technical Maintenance](./maintenance.md)
|
### 🛠️ [Technical Maintenance](./maintenance.md)
|
||||||
Guide on code generation (GraphQL Codegen), running Jest tests, and local Docker builds.
|
Guide on code generation (GraphQL Codegen), running Jest tests, and local Docker builds.
|
||||||
|
|
||||||
### 💻 [Local Development Environment](./local_development.md)
|
|
||||||
Detailed instructions for setting up a fully isolated local development environment with Zabbix using Docker Compose.
|
|
||||||
|
|
||||||
### 🧪 [Test Specification](../tests.md)
|
### 🧪 [Test Specification](../tests.md)
|
||||||
Detailed list of test cases, categories (Unit, Integration, E2E), and coverage checklist.
|
Detailed list of test cases, categories (Unit, Integration, E2E), and coverage checklist.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
# 💻 Local Development Environment
|
|
||||||
|
|
||||||
This guide provides detailed instructions on how to set up a fully isolated local development environment for the Zabbix GraphQL API using Docker Compose.
|
|
||||||
|
|
||||||
## 🚀 Overview
|
|
||||||
The project includes a Docker Compose profile that launches a complete Zabbix stack alongside the GraphQL API and MCP server. This allows you to develop and test features against different Zabbix versions without needing an external Zabbix installation.
|
|
||||||
|
|
||||||
### Included Services
|
|
||||||
- **`zabbix-graphql-api`**: The main GraphQL server.
|
|
||||||
- **`apollo-mcp-server`**: The Model Context Protocol server for AI integration.
|
|
||||||
- **`postgres-server`**: PostgreSQL 16 database for Zabbix.
|
|
||||||
- **`zabbix-server`**: Zabbix Server (PostgreSQL version).
|
|
||||||
- **`zabbix-web`**: Zabbix Web interface (Nginx/PostgreSQL).
|
|
||||||
|
|
||||||
## 🛠️ Step-by-Step Setup
|
|
||||||
|
|
||||||
### 1. Start the Environment
|
|
||||||
Use the `zabbix-local` profile to start all services. You can optionally specify the Zabbix version using the `ZABBIX_VERSION` environment variable.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start the latest 7.0 (default)
|
|
||||||
docker compose --profile zabbix-local up -d
|
|
||||||
|
|
||||||
# Start Zabbix 6.0 (LTS)
|
|
||||||
ZABBIX_VERSION=alpine-6.0-latest docker compose --profile zabbix-local up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Zabbix
|
|
||||||
Once the containers are running, you need to perform a minimal setup in the Zabbix UI:
|
|
||||||
|
|
||||||
1. Open `http://localhost:8080` in your browser.
|
|
||||||
2. Log in with the default credentials:
|
|
||||||
- **User**: `Admin`
|
|
||||||
- **Password**: `zabbix`
|
|
||||||
3. Navigate to **Administration** > **Users** > **API tokens**.
|
|
||||||
4. Create a new token for the `Admin` user (or a dedicated service user) and copy it.
|
|
||||||
5. **Create Base Host Group**: The API requires a base host group to exist for importing devices.
|
|
||||||
- Navigate to **Configuration** > **Host groups** (or **Data collection** > **Host groups** in Zabbix 7.0).
|
|
||||||
- Create a host group named `Roadwork` (or the value of `ZABBIX_EDGE_DEVICE_BASE_GROUP` in your `.env`).
|
|
||||||
- **Note**: If your base group is nested (e.g., `Roadwork/Devices`), ensure the full path exists. Zabbix does not automatically create parent groups when creating child groups via the API.
|
|
||||||
|
|
||||||
### 3. Connect the API
|
|
||||||
To connect the GraphQL API to your local Zabbix instance, update your `.env` file with the following values (referenced in `samples/zabbix-local.env`):
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Internal Docker network URL
|
|
||||||
ZABBIX_BASE_URL=http://zabbix-web:8080/
|
|
||||||
|
|
||||||
# The token you created in Step 2
|
|
||||||
ZABBIX_PRIVILEGE_ESCALATION_TOKEN=your-newly-created-token
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Restart the API
|
|
||||||
Apply the configuration by restarting the API container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose restart zabbix-graphql-api
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Verification
|
|
||||||
You can verify the setup by running a simple query against the local API:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-d '{"query": "{ apiVersion zabbixVersion }"}' \
|
|
||||||
http://localhost:4001/
|
|
||||||
```
|
|
||||||
|
|
||||||
The `zabbixVersion` should match the version you specified in `ZABBIX_VERSION`.
|
|
||||||
|
|
||||||
## 💡 Advanced Usage
|
|
||||||
|
|
||||||
### Data Persistence
|
|
||||||
Zabbix data is stored in a named Docker volume called `zbx_db_data`. This ensures that your configuration, templates, and hosts are preserved even if you stop or remove the containers.
|
|
||||||
|
|
||||||
To perform a clean reset of the environment:
|
|
||||||
```bash
|
|
||||||
docker compose --profile zabbix-local down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Multiple Versions
|
|
||||||
The local environment is perfect for testing the API's version-specific logic (e.g. `history.push` vs `zabbix_sender`). Simply change the `ZABBIX_VERSION` variable and restart the stack.
|
|
||||||
|
|
||||||
Supported tags can be found on the [Zabbix Docker Hub](https://hub.docker.com/u/zabbix).
|
|
||||||
|
|
||||||
---
|
|
||||||
**Related Guide**: [Technical Maintenance](./maintenance.md)
|
|
||||||
**Related Reference**: [Zabbix Version Compatibility](../../README.md#-zabbix-version-compatibility)
|
|
||||||
|
|
@ -34,9 +34,6 @@ We use [Jest](https://jestjs.io/) for unit and integration testing.
|
||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Local Development Setup
|
|
||||||
For running integration tests against a real Zabbix instance, it is recommended to use the [Local Development Environment](./local_development.md). This setup allows you to test the API against specific Zabbix versions (e.g. 6.0, 7.0) in an isolated way.
|
|
||||||
|
|
||||||
#### Adding New Tests
|
#### Adding New Tests
|
||||||
- **Location**: Place new test files in `src/test/` with the `.test.ts` extension.
|
- **Location**: Place new test files in `src/test/` with the `.test.ts` extension.
|
||||||
- **Coverage**: Ensure you cover both successful operations and error scenarios.
|
- **Coverage**: Ensure you cover both successful operations and error scenarios.
|
||||||
|
|
|
||||||
13
roadmap.md
13
roadmap.md
|
|
@ -6,21 +6,12 @@ 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.
|
||||||
|
|
||||||
- **🐳 Local Development Environment**: Integrated a complete Zabbix stack into the Docker Compose configuration using profiles.
|
|
||||||
- *Feature*: Support for multiple Zabbix versions (6.0, 6.4, 7.0+) for development and testing.
|
|
||||||
- *Implementation*: Added `zabbix-local` profile and `ZABBIX_VERSION` dynamic image tagging.
|
|
||||||
|
|
||||||
- **🔧 Multi-Version Compatibility**: Verified and enhanced support for Zabbix 6.0 LTS, 6.2, 6.4, and 7.0 LTS.
|
|
||||||
- *Feature*: Automatic fallback logic for older Zabbix versions (auth, name-based matching, manual expansion).
|
|
||||||
- *Verification*: Full regression and smoketest suites passed across all mentioned versions.
|
|
||||||
|
|
||||||
## 📅 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*:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Zabbix Connection for local Docker Compose profile (zabbix-local)
|
|
||||||
# Use the contents of this file in your .env or pass them to docker compose
|
|
||||||
# when running with 'docker compose --profile zabbix-local up'
|
|
||||||
|
|
||||||
# Internal URL for the API to connect to the local Zabbix container
|
|
||||||
ZABBIX_BASE_URL=http://zabbix-web:8080/
|
|
||||||
|
|
||||||
# Note: After Zabbix starts, you must log in to http://localhost:8080 (Admin/zabbix)
|
|
||||||
# and create an API token to use as ZABBIX_PRIVILEGE_ESCALATION_TOKEN.
|
|
||||||
ZABBIX_PRIVILEGE_ESCALATION_TOKEN=your-newly-created-zabbix-token
|
|
||||||
|
|
@ -166,22 +166,32 @@ export function createResolvers(): Resolvers {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie, dataSources
|
cookie, dataSources
|
||||||
}: any, info: any) => {
|
}: any, info: any) => {
|
||||||
|
let params: any = {}
|
||||||
if (args.hostids) {
|
if (args.hostids) {
|
||||||
// @ts-ignore
|
params.templateids = args.hostids
|
||||||
args.templateids = args.hostids
|
}
|
||||||
delete args.hostids
|
if (args.name_pattern) {
|
||||||
|
params.search = {
|
||||||
|
name: args.name_pattern
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
|
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
|
||||||
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output);
|
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
||||||
},
|
},
|
||||||
|
|
||||||
allTemplateGroups: async (_parent: any, args: any, {
|
allTemplateGroups: async (_parent: any, args: any, {
|
||||||
zabbixAuthToken,
|
zabbixAuthToken,
|
||||||
cookie
|
cookie
|
||||||
}: any) => {
|
}: any) => {
|
||||||
|
let params: any = {}
|
||||||
|
if (args.name_pattern) {
|
||||||
|
params.search = {
|
||||||
|
name: args.name_pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
|
return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
|
||||||
.executeRequestThrowError(zabbixAPI, new ParsedArgs(args));
|
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,6 @@ 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,10 +81,6 @@ 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,33 +100,4 @@ 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)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,8 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
||||||
this.impliedFields.set("hostType", ["tags"]);
|
this.impliedFields.set("hostType", ["tags"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
|
|
||||||
const version = await zabbixAPI.getVersion();
|
|
||||||
const result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
|
||||||
|
|
||||||
if (!isZabbixErrorResult(result) && version < "6.2.0") {
|
|
||||||
const hosts = result as any[];
|
|
||||||
for (const host of hosts) {
|
|
||||||
if (host.groups) {
|
|
||||||
host.hostgroups = host.groups;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||||
const params: any = {
|
return this.optimizeZabbixParams({
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
selectParentTemplates: [
|
selectParentTemplates: [
|
||||||
"templateid",
|
"templateid",
|
||||||
|
|
@ -60,15 +45,11 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
||||||
"hostid",
|
"hostid",
|
||||||
"host",
|
"host",
|
||||||
"name",
|
"name",
|
||||||
|
"hostgroups",
|
||||||
"description",
|
"description",
|
||||||
|
"parentTemplates"
|
||||||
]
|
]
|
||||||
};
|
}, output);
|
||||||
|
|
||||||
// Zabbix 6.0 compatibility: use selectGroups instead of selectHostGroups
|
|
||||||
// We include both, Zabbix will ignore the unsupported one.
|
|
||||||
params.selectGroups = ["groupid", "name"];
|
|
||||||
|
|
||||||
return this.optimizeZabbixParams(params, output);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +102,10 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
||||||
"hostid",
|
"hostid",
|
||||||
"host",
|
"host",
|
||||||
"name",
|
"name",
|
||||||
|
"hostgroups",
|
||||||
|
"items",
|
||||||
"description",
|
"description",
|
||||||
|
"parentTemplates"
|
||||||
],
|
],
|
||||||
}, output);
|
}, output);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
|
import {ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
|
||||||
import {ZabbixAPI} from "./zabbix-api.js";
|
import {ZabbixAPI} from "./zabbix-api.js";
|
||||||
import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js";
|
import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js";
|
||||||
import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values.js";
|
import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values.js";
|
||||||
|
|
@ -18,10 +18,6 @@ export class ZabbixRequestWithPermissions<T extends ZabbixResult, A extends Pars
|
||||||
return this.prepResult;
|
return this.prepResult;
|
||||||
}
|
}
|
||||||
async assureUserPermissions(zabbixAPI: ZabbixAPI) {
|
async assureUserPermissions(zabbixAPI: ZabbixAPI) {
|
||||||
if (this.authToken && this.authToken === Config.ZABBIX_PRIVILEGE_ESCALATION_TOKEN) {
|
|
||||||
// Bypass permission check for the privilege escalation token as it is assumed to have required rights
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (this.permissionsNeeded &&
|
if (this.permissionsNeeded &&
|
||||||
!await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) {
|
!await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -79,31 +75,7 @@ class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGro
|
||||||
super("usergroup.get.permissions", authToken, cookie);
|
super("usergroup.get.permissions", authToken, cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixUserGroupResponse[] | ZabbixErrorResult> {
|
createZabbixParams(args?: ParsedArgs) {
|
||||||
const version = await zabbixAPI.getVersion();
|
|
||||||
const result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
|
||||||
|
|
||||||
if (!isZabbixErrorResult(result) && version < "6.2.0") {
|
|
||||||
// Map 6.0 'rights' to 6.2+ style 'templategroup_rights'
|
|
||||||
const usergroups = result as ZabbixUserGroupResponse[];
|
|
||||||
for (const usergroup of usergroups) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (usergroup.rights) {
|
|
||||||
// @ts-ignore
|
|
||||||
usergroup.templategroup_rights = usergroup.rights.map((r: any) => ({
|
|
||||||
id: r.id,
|
|
||||||
permission: r.permission
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createZabbixParams(args?: ParsedArgs) {
|
|
||||||
// We don't have access to ZabbixAPI here to check version easily,
|
|
||||||
// but we can just include both selectRights and selectTemplateGroupRights.
|
|
||||||
// Zabbix 6.2+ will ignore selectRights, Zabbix 6.0 will ignore selectTemplateGroupRights.
|
|
||||||
return {
|
return {
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
"output": [
|
"output": [
|
||||||
|
|
@ -115,8 +87,7 @@ class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGro
|
||||||
"selectTemplateGroupRights": [
|
"selectTemplateGroupRights": [
|
||||||
"id",
|
"id",
|
||||||
"permission"
|
"permission"
|
||||||
],
|
]
|
||||||
"selectRights": "extend"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +110,7 @@ export class ZabbixPermissionsHelper {
|
||||||
const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
|
const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
|
||||||
|
|
||||||
// Prepare the list of templateIds that are not loaded yet
|
// Prepare the list of templateIds that are not loaded yet
|
||||||
const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => (usergroup.templategroup_rights || []).map(templateGroupRight => templateGroupRight.id)));
|
const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => usergroup.templategroup_rights.map(templateGroupRight => templateGroupRight.id)));
|
||||||
|
|
||||||
// Remove all templateIds that are already in the permissionObjectNameCache
|
// Remove all templateIds that are already in the permissionObjectNameCache
|
||||||
templateIdsToLoad.forEach(id => {
|
templateIdsToLoad.forEach(id => {
|
||||||
|
|
@ -174,7 +145,7 @@ export class ZabbixPermissionsHelper {
|
||||||
|
|
||||||
let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames);
|
let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames);
|
||||||
|
|
||||||
(usergroup.templategroup_rights || []).forEach(templateGroupPermission => {
|
usergroup.templategroup_rights.forEach(templateGroupPermission => {
|
||||||
const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id);
|
const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id);
|
||||||
if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) {
|
if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) {
|
||||||
const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber;
|
const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ class ZabbixRequestBody {
|
||||||
public method
|
public method
|
||||||
public id = 1
|
public id = 1
|
||||||
public params?: ZabbixParams
|
public params?: ZabbixParams
|
||||||
public auth?: string
|
|
||||||
|
|
||||||
constructor(method: string) {
|
constructor(method: string) {
|
||||||
this.method = method;
|
this.method = method;
|
||||||
|
|
@ -139,7 +138,6 @@ export class ParsedArgs {
|
||||||
(<any>result).search.name = this.name_pattern;
|
(<any>result).search.name = this.name_pattern;
|
||||||
(<any>result).search.host = this.name_pattern;
|
(<any>result).search.host = this.name_pattern;
|
||||||
(<any>result).searchByAny = true;
|
(<any>result).searchByAny = true;
|
||||||
(<any>result).searchWildcardsEnabled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -179,12 +177,6 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
if (params.output) {
|
if (params.output) {
|
||||||
if (Array.isArray(params.output)) {
|
if (Array.isArray(params.output)) {
|
||||||
params.output = params.output.filter(field => topLevelOutput.includes(field));
|
params.output = params.output.filter(field => topLevelOutput.includes(field));
|
||||||
// Add any missing top-level fields that are needed
|
|
||||||
topLevelOutput.forEach(top => {
|
|
||||||
if (!params.output.includes(top)) {
|
|
||||||
params.output.push(top);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (params.output === "extend") {
|
} else if (params.output === "extend") {
|
||||||
params.output = topLevelOutput;
|
params.output = topLevelOutput;
|
||||||
}
|
}
|
||||||
|
|
@ -219,15 +211,10 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
const p = zabbixParams ?? this.createZabbixParams(args, output);
|
const p = zabbixParams ?? this.createZabbixParams(args, output);
|
||||||
params = Array.isArray(p) ? p : {...this.requestBodyTemplate.params, ...p}
|
params = Array.isArray(p) ? p : {...this.requestBodyTemplate.params, ...p}
|
||||||
}
|
}
|
||||||
const body: ZabbixRequestBody = params ? {
|
return params ? {
|
||||||
...this.requestBodyTemplate,
|
...this.requestBodyTemplate,
|
||||||
params: params
|
params: params
|
||||||
} : {...this.requestBodyTemplate}
|
} : this.requestBodyTemplate
|
||||||
|
|
||||||
if (this.authToken) {
|
|
||||||
body.auth = this.authToken
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
};
|
};
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
|
|
@ -259,17 +246,6 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
|
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||||
if (this.path !== "apiinfo.version") {
|
|
||||||
const version = await zabbixAPI.getVersion();
|
|
||||||
if (version < "6.2.0") {
|
|
||||||
if (this.path.startsWith("templategroup.")) {
|
|
||||||
this.path = this.path.replace("templategroup.", "hostgroup.");
|
|
||||||
this.method = this.path.split(".", 2).join(".");
|
|
||||||
this.requestBodyTemplate.method = this.method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prepareResult = await this.prepare(zabbixAPI, args);
|
let prepareResult = await this.prepare(zabbixAPI, args);
|
||||||
if (prepareResult) {
|
if (prepareResult) {
|
||||||
return prepareResult;
|
return prepareResult;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ 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 {GroupHelper, ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
|
import {ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
|
||||||
import {ApiErrorCode} from "../model/model_enum_values.js";
|
import {ApiErrorCode} from "../model/model_enum_values.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,30 +77,13 @@ export class ZabbixExportUserGroupsRequest extends ZabbixPrepareGetTemplatesAndH
|
||||||
...super.createZabbixParams(args),
|
...super.createZabbixParams(args),
|
||||||
output: "extend",
|
output: "extend",
|
||||||
selectTemplateGroupRights: "extend",
|
selectTemplateGroupRights: "extend",
|
||||||
selectHostGroupRights: "extend",
|
selectHostGroupRights: "extend"
|
||||||
selectRights: "extend"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixExportUserGroupArgs): Promise<ZabbixErrorResult | UserGroup[]> {
|
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixExportUserGroupArgs): Promise<ZabbixErrorResult | UserGroup[]> {
|
||||||
const version = await zabbixAPI.getVersion();
|
|
||||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
||||||
if (!isZabbixErrorResult(result)) {
|
if (!isZabbixErrorResult(result)) {
|
||||||
if (version < "6.2.0") {
|
|
||||||
for (let userGroup of result) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (userGroup.rights) {
|
|
||||||
// In 6.0, 'rights' contains both host and template group permissions
|
|
||||||
// @ts-ignore
|
|
||||||
userGroup.hostgroup_rights = userGroup.rights.map((r: any) => ({
|
|
||||||
id: r.id,
|
|
||||||
permission: r.permission
|
|
||||||
}));
|
|
||||||
userGroup.templategroup_rights = []; // Or duplicates?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let userGroup of result) {
|
for (let userGroup of result) {
|
||||||
for (let template_permission of userGroup.templategroup_rights || []) {
|
for (let template_permission of userGroup.templategroup_rights || []) {
|
||||||
for (let templategroup of this.templategroups) {
|
for (let templategroup of this.templategroups) {
|
||||||
|
|
@ -171,55 +153,19 @@ 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 paramsObj: any = {
|
let params = new ZabbixCreateOrUpdateParams({
|
||||||
name: userGroup.name,
|
name: userGroup.name,
|
||||||
gui_access: userGroup.gui_access,
|
gui_access: userGroup.gui_access,
|
||||||
users_status: userGroup.users_status,
|
users_status: userGroup.users_status,
|
||||||
};
|
hostgroup_rights: hostgroup_rights.hostgroup_rights,
|
||||||
|
templategroup_rights: templategroup_rights.templategroup_rights,
|
||||||
if (version < "6.2.0") {
|
}, args?.dryRun)
|
||||||
paramsObj.rights = [
|
|
||||||
...hostgroup_rights.hostgroup_rights,
|
|
||||||
...templategroup_rights.templategroup_rights
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
paramsObj.hostgroup_rights = hostgroup_rights.hostgroup_rights;
|
|
||||||
paramsObj.templategroup_rights = templategroup_rights.templategroup_rights;
|
|
||||||
}
|
|
||||||
|
|
||||||
let params = new ZabbixCreateOrUpdateParams(paramsObj, args?.dryRun)
|
|
||||||
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
|
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
|
||||||
if (isZabbixErrorResult(result)) {
|
if (isZabbixErrorResult(result)) {
|
||||||
|
|
||||||
|
|
@ -268,52 +214,34 @@ 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 && hostgroup.uuid === hostgroup_right.uuid) {
|
if (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: matchedId!,
|
id: Number(hostgroup.groupid),
|
||||||
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 provided name=${hostgroup_right.name}`,
|
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match`,
|
||||||
data: hostgroup_right,
|
data: hostgroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
if (!success) {
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
|
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
|
||||||
message: `Hostgroup with UUID ${hostgroup_right.uuid} ${hostgroup_right.name ? "or name " + hostgroup_right.name : ""} not found`,
|
message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`,
|
||||||
data: hostgroup_right,
|
data: hostgroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -334,52 +262,33 @@ 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 && templategroup.uuid === templategroup_right.uuid) {
|
if (templategroup.uuid == templategroup_right.uuid) {
|
||||||
matchedId = Number(templategroup.groupid);
|
|
||||||
matchedName = templategroup.name;
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
result.push(
|
||||||
{
|
{
|
||||||
id: matchedId!,
|
id: Number(templategroup.groupid),
|
||||||
permission: templategroup_right.permission,
|
permission: templategroup_right.permission,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
success = true;
|
||||||
if (templategroup_right.name && templategroup_right.name != matchedName) {
|
matchedName = templategroup.name
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success && 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 provided name=${templategroup_right.name}`,
|
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match`,
|
||||||
data: templategroup_right,
|
data: templategroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
if (!success) {
|
||||||
errors.push(
|
errors.push(
|
||||||
{
|
{
|
||||||
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
|
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
|
||||||
message: `Templategroup with UUID ${templategroup_right.uuid} ${templategroup_right.name ? "or name " + templategroup_right.name : ""} not found`,
|
message: `Templategroup with UUID ${templategroup_right.uuid} not found`,
|
||||||
data: templategroup_right,
|
data: templategroup_right,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -408,17 +317,6 @@ 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 => {
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,6 @@ export class RegressionTestExecutor {
|
||||||
const hostName = "REG_HOST_" + Math.random().toString(36).substring(7);
|
const hostName = "REG_HOST_" + Math.random().toString(36).substring(7);
|
||||||
const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7);
|
const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
const regTemplateName = "REG_TEMP_" + Math.random().toString(36).substring(7);
|
|
||||||
const httpTempName = "REG_HTTP_" + Math.random().toString(36).substring(7);
|
|
||||||
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
|
|
||||||
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
|
|
||||||
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
|
|
||||||
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
|
|
||||||
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
|
|
||||||
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
|
|
||||||
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
|
|
||||||
const devHostNameWithTag = "REG_DEV_WITH_TAG_" + Math.random().toString(36).substring(7);
|
|
||||||
const devHostNameWithoutTag = "REG_DEV_WITHOUT_TAG_" + Math.random().toString(36).substring(7);
|
|
||||||
const pushHostName = "REG_PUSH_HOST_" + Math.random().toString(36).substring(7);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Regression 1: Locations query argument order
|
// Regression 1: Locations query argument order
|
||||||
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
|
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
|
||||||
|
|
@ -57,6 +44,7 @@ export class RegressionTestExecutor {
|
||||||
|
|
||||||
// Regression 2: Template lookup by technical name
|
// Regression 2: Template lookup by technical name
|
||||||
// Verifies that importHosts can link templates using their technical name (host)
|
// Verifies that importHosts can link templates using their technical name (host)
|
||||||
|
const regTemplateName = "REG_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
const regGroupName = "Templates/Roadwork/Devices";
|
const regGroupName = "Templates/Roadwork/Devices";
|
||||||
const hostGroupName = "Roadwork/Devices";
|
const hostGroupName = "Roadwork/Devices";
|
||||||
|
|
||||||
|
|
@ -67,7 +55,7 @@ export class RegressionTestExecutor {
|
||||||
|
|
||||||
const tempResult = await TemplateImporter.importTemplates([{
|
const tempResult = await TemplateImporter.importTemplates([{
|
||||||
host: regTemplateName,
|
host: regTemplateName,
|
||||||
name: "Regression Test Template " + regTemplateName,
|
name: "Regression Test Template",
|
||||||
groupNames: [regGroupName]
|
groupNames: [regGroupName]
|
||||||
}], zabbixAuthToken, cookie);
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
|
@ -81,9 +69,10 @@ export class RegressionTestExecutor {
|
||||||
|
|
||||||
// Regression 3: HTTP Agent URL support
|
// Regression 3: HTTP Agent URL support
|
||||||
// Verifies that templates with HTTP Agent items (including URL) can be imported
|
// Verifies that templates with HTTP Agent items (including URL) can be imported
|
||||||
|
const httpTempName = "REG_HTTP_" + Math.random().toString(36).substring(7);
|
||||||
const httpTempResult = await TemplateImporter.importTemplates([{
|
const httpTempResult = await TemplateImporter.importTemplates([{
|
||||||
host: httpTempName,
|
host: httpTempName,
|
||||||
name: "Regression HTTP Template " + httpTempName,
|
name: "Regression HTTP Template",
|
||||||
groupNames: [regGroupName],
|
groupNames: [regGroupName],
|
||||||
items: [{
|
items: [{
|
||||||
name: "HTTP Master",
|
name: "HTTP Master",
|
||||||
|
|
@ -105,9 +94,12 @@ export class RegressionTestExecutor {
|
||||||
if (!httpSuccess) success = false;
|
if (!httpSuccess) success = false;
|
||||||
|
|
||||||
// Regression 4: User Macro assignment for host and template creation
|
// Regression 4: User Macro assignment for host and template creation
|
||||||
|
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
const macroTempResult = await TemplateImporter.importTemplates([{
|
const macroTempResult = await TemplateImporter.importTemplates([{
|
||||||
host: macroTemplateName,
|
host: macroTemplateName,
|
||||||
name: "Regression Macro Template " + macroTemplateName,
|
name: "Regression Macro Template",
|
||||||
groupNames: [regGroupName],
|
groupNames: [regGroupName],
|
||||||
macros: [
|
macros: [
|
||||||
{ macro: "{$TEMP_MACRO}", value: "temp_value" }
|
{ macro: "{$TEMP_MACRO}", value: "temp_value" }
|
||||||
|
|
@ -221,9 +213,12 @@ export class RegressionTestExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression 6: Item Metadata (preprocessing, units, description, error)
|
// Regression 6: Item Metadata (preprocessing, units, description, error)
|
||||||
|
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
const metaTempResult = await TemplateImporter.importTemplates([{
|
const metaTempResult = await TemplateImporter.importTemplates([{
|
||||||
host: metaTempName,
|
host: metaTempName,
|
||||||
name: "Regression Meta Template " + metaTempName,
|
name: "Regression Meta Template",
|
||||||
groupNames: [regGroupName],
|
groupNames: [regGroupName],
|
||||||
items: [{
|
items: [{
|
||||||
name: "Meta Item",
|
name: "Meta Item",
|
||||||
|
|
@ -314,17 +309,19 @@ export class RegressionTestExecutor {
|
||||||
// 3. Test indirect dependencies: state implies items
|
// 3. Test indirect dependencies: state implies items
|
||||||
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
|
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
|
||||||
const hasSelectItems3 = "selectItems" in testParams3;
|
const hasSelectItems3 = "selectItems" in testParams3;
|
||||||
|
const hasOutput3 = Array.isArray(testParams3.output) && testParams3.output.includes("hostid") && testParams3.output.includes("items");
|
||||||
|
|
||||||
optSuccess = optSuccess && hasSelectItems3;
|
optSuccess = optSuccess && hasSelectItems3 && hasOutput3;
|
||||||
|
|
||||||
// 4. Test indirect dependencies: deviceType implies tags
|
// 4. Test indirect dependencies: deviceType implies tags
|
||||||
const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]);
|
const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]);
|
||||||
const hasSelectTags4 = "selectTags" in testParams4;
|
const hasSelectTags4 = "selectTags" in testParams4;
|
||||||
|
const hasOutput4 = Array.isArray(testParams4.output) && testParams4.output.includes("hostid");
|
||||||
|
|
||||||
optSuccess = optSuccess && hasSelectTags4;
|
optSuccess = optSuccess && hasSelectTags4 && hasOutput4;
|
||||||
|
|
||||||
if (!optSuccess) {
|
if (!optSuccess) {
|
||||||
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasSelectTags4: ${hasSelectTags4}`);
|
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}, hasSelectTags4: ${hasSelectTags4}, hasOutput4: ${hasOutput4}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||||
|
|
@ -360,9 +357,10 @@ export class RegressionTestExecutor {
|
||||||
if (!emptySuccess) success = false;
|
if (!emptySuccess) success = false;
|
||||||
|
|
||||||
// Regression 9: Dependent Items in Templates
|
// Regression 9: Dependent Items in Templates
|
||||||
|
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
const depTempResult = await TemplateImporter.importTemplates([{
|
const depTempResult = await TemplateImporter.importTemplates([{
|
||||||
host: depTempName,
|
host: depTempName,
|
||||||
name: "Regression Dependent Template " + depTempName,
|
name: "Regression Dependent Template",
|
||||||
groupNames: [regGroupName],
|
groupNames: [regGroupName],
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|
@ -392,9 +390,12 @@ export class RegressionTestExecutor {
|
||||||
if (!depSuccess) success = false;
|
if (!depSuccess) success = false;
|
||||||
|
|
||||||
// Regression 10: State sub-properties retrieval (Optimization indirect dependency)
|
// Regression 10: State sub-properties retrieval (Optimization indirect dependency)
|
||||||
|
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
|
||||||
|
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
const stateTempResult = await TemplateImporter.importTemplates([{
|
const stateTempResult = await TemplateImporter.importTemplates([{
|
||||||
host: stateTempName,
|
host: stateTempName,
|
||||||
name: "Regression State Template " + stateTempName,
|
name: "Regression State Template",
|
||||||
groupNames: [regGroupName],
|
groupNames: [regGroupName],
|
||||||
tags: [{ tag: "deviceType", value: "GenericDevice" }],
|
tags: [{ tag: "deviceType", value: "GenericDevice" }],
|
||||||
items: [{
|
items: [{
|
||||||
|
|
@ -481,6 +482,9 @@ export class RegressionTestExecutor {
|
||||||
|
|
||||||
// Regression 12: allDevices deviceType filter
|
// Regression 12: allDevices deviceType filter
|
||||||
// Verifies that allDevices only returns hosts with a deviceType tag
|
// Verifies that allDevices only returns hosts with a deviceType tag
|
||||||
|
const devHostNameWithTag = "REG_DEV_WITH_TAG_" + Math.random().toString(36).substring(7);
|
||||||
|
const devHostNameWithoutTag = "REG_DEV_WITHOUT_TAG_" + Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
// Get groupid for hostGroupName
|
// Get groupid for hostGroupName
|
||||||
const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie)
|
const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie)
|
||||||
.executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName }));
|
.executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName }));
|
||||||
|
|
@ -528,13 +532,7 @@ export class RegressionTestExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression 13: pushHistory mutation
|
// Regression 13: pushHistory mutation
|
||||||
let pushSuccess = false;
|
const pushHostName = "REG_PUSH_HOST_" + Math.random().toString(36).substring(7);
|
||||||
const version = await zabbixAPI.getVersion();
|
|
||||||
|
|
||||||
if (version < "7.0.0") {
|
|
||||||
logger.info(`REG-PUSH: Skipping pushHistory test as it is not supported on Zabbix version ${version}`);
|
|
||||||
pushSuccess = true; // Mark as success for old versions to allow overall test success
|
|
||||||
} else {
|
|
||||||
const pushItemKey = "trap.json";
|
const pushItemKey = "trap.json";
|
||||||
|
|
||||||
// Create host
|
// Create host
|
||||||
|
|
@ -545,6 +543,7 @@ export class RegressionTestExecutor {
|
||||||
templateNames: []
|
templateNames: []
|
||||||
}], zabbixAuthToken, cookie);
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
|
let pushSuccess = false;
|
||||||
if (pushHostResult?.length && pushHostResult[0].hostid) {
|
if (pushHostResult?.length && pushHostResult[0].hostid) {
|
||||||
const pushHostId = pushHostResult[0].hostid;
|
const pushHostId = pushHostResult[0].hostid;
|
||||||
|
|
||||||
|
|
@ -569,18 +568,12 @@ export class RegressionTestExecutor {
|
||||||
const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams);
|
const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams);
|
||||||
pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success";
|
pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup push host
|
|
||||||
await HostDeleter.deleteHosts([Number(pushHostId)], null, zabbixAuthToken, cookie);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
steps.push({
|
steps.push({
|
||||||
name: "REG-PUSH: pushHistory mutation",
|
name: "REG-PUSH: pushHistory mutation",
|
||||||
success: pushSuccess,
|
success: pushSuccess,
|
||||||
message: version < "7.0.0"
|
message: pushSuccess ? "Successfully pushed history data to trapper item" : "Failed to push history data"
|
||||||
? `Skipped (not supported on ${version})`
|
|
||||||
: (pushSuccess ? "Successfully pushed history data to trapper item" : "Failed to push history data")
|
|
||||||
});
|
});
|
||||||
if (!pushSuccess) success = false;
|
if (!pushSuccess) success = false;
|
||||||
|
|
||||||
|
|
@ -620,22 +613,6 @@ export class RegressionTestExecutor {
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || String(error)
|
message: error.message || String(error)
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
// Cleanup
|
|
||||||
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, metaHostName, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, devHostNameWithTag, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, devHostNameWithoutTag, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, pushHostName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, regTemplateName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, depTempName, zabbixAuthToken, cookie);
|
|
||||||
await TemplateDeleter.deleteTemplates(null, stateTempName, zabbixAuthToken, cookie);
|
|
||||||
await HostDeleter.deleteHosts(null, stateHostName, zabbixAuthToken, cookie);
|
|
||||||
// We don't delete the group here as it might be shared or used by other tests in this run
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,11 @@ export class SmoketestExecutor {
|
||||||
templateNames: [templateName]
|
templateNames: [templateName]
|
||||||
}], zabbixAuthToken, cookie);
|
}], zabbixAuthToken, cookie);
|
||||||
|
|
||||||
const hostSuccess = !!hostResult?.length && !hostResult[0].error && !!hostResult[0].hostid;
|
const hostSuccess = !!hostResult?.length && !hostResult[0].error;
|
||||||
steps.push({
|
steps.push({
|
||||||
name: "Create and Link Host",
|
name: "Create and Link Host",
|
||||||
success: hostSuccess,
|
success: hostSuccess,
|
||||||
message: hostSuccess ? `Host ${hostName} created and linked to ${templateName}` : `Failed: ${hostResult?.[0]?.error?.message || hostResult?.[0]?.message || "Unknown error"}`
|
message: hostSuccess ? `Host ${hostName} created and linked to ${templateName}` : `Failed: ${hostResult?.[0]?.error?.message || "Unknown error"}`
|
||||||
});
|
});
|
||||||
if (!hostSuccess) success = false;
|
if (!hostSuccess) success = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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")
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -64,12 +63,4 @@ 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,7 +6,6 @@ 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,7 +6,6 @@ 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,7 +8,6 @@ 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,7 +7,6 @@ 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,7 +7,6 @@ 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",
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
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"),
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ 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",
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
@ -91,7 +90,7 @@ describe("Query Optimization", () => {
|
||||||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||||
body: expect.objectContaining({
|
body: expect.objectContaining({
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
output: expect.arrayContaining(["hostid", "tags"]),
|
output: ["hostid"],
|
||||||
selectTags: expect.any(Array)
|
selectTags: expect.any(Array)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -188,7 +187,7 @@ describe("Query Optimization", () => {
|
||||||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||||
body: expect.objectContaining({
|
body: expect.objectContaining({
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
output: expect.arrayContaining(["hostid", "tags"]),
|
output: ["hostid"],
|
||||||
selectTags: expect.any(Array)
|
selectTags: expect.any(Array)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
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"),
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
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"),
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ 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,7 +6,6 @@ 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,7 +7,6 @@ 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"
|
||||||
|
|
@ -83,11 +82,8 @@ describe("Template Resolver", () => {
|
||||||
method: "template.get",
|
method: "template.get",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
search: {
|
search: {
|
||||||
name: "Template",
|
name: "Template"
|
||||||
host: "Template"
|
}
|
||||||
},
|
|
||||||
searchByAny: true,
|
|
||||||
searchWildcardsEnabled: true
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
@ -109,11 +105,8 @@ describe("Template Resolver", () => {
|
||||||
method: "template.get",
|
method: "template.get",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
search: {
|
search: {
|
||||||
name: "Temp%1",
|
name: "Temp%1"
|
||||||
host: "Temp%1"
|
}
|
||||||
},
|
|
||||||
searchByAny: true,
|
|
||||||
searchWildcardsEnabled: true
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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,7 +8,6 @@ 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(),
|
||||||
|
|
@ -44,8 +43,7 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
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.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.rights).toHaveLength(2);
|
|
||||||
expect(createCall[1].body.params.rights).toContainEqual({ id: 201, permission: Permission.Read });
|
|
||||||
expect(createCall[1].body.params.rights).toContainEqual({ id: 202, permission: Permission.Read });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -8,7 +8,6 @@ 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