Initial commit: Extract base Zabbix GraphQl - API functionality from VCR Project and add dynamic schema samples

This commit is contained in:
Andreas Hilbig 2026-01-05 21:05:35 +01:00
commit 92ffe71684
42 changed files with 4234 additions and 0 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
node_modules
npm-debug.log

132
.gitignore vendored Normal file
View file

@ -0,0 +1,132 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.vscode/settings.json

9
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,9 @@
# Default ignored files
/shelf/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

6
.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TypeScriptCompiler">
<option name="nodeInterpreterTextField" value="wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v22.14.0/bin/node" />
</component>
</project>

11
.idea/go.imports.xml generated Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

6
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/zabbix-graphql-api.iml" filepath="$PROJECT_DIR$/zabbix-graphql-api.iml" />
</modules>
</component>
</project>

15
.idea/runConfigurations/codegen.xml generated Normal file
View file

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="codegen" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="codegen" />
</scripts>
<node-interpreter value="wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v22.14.0/bin/node" />
<envs />
<EXTENSION ID="com.intellij.javascript.debugger.execution.StartBrowserRunConfigurationExtension">
<browser with-js-debugger="true" />
</EXTENSION>
<method v="2" />
</configuration>
</component>

15
.idea/runConfigurations/compile.xml generated Normal file
View file

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="compile" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="compile" />
</scripts>
<node-interpreter value="wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v22.14.0/bin/node" />
<envs />
<EXTENSION ID="com.intellij.javascript.debugger.execution.StartBrowserRunConfigurationExtension">
<browser with-js-debugger="true" />
</EXTENSION>
<method v="2" />
</configuration>
</component>

17
.idea/runConfigurations/index_ts.xml generated Normal file
View file

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="index.ts" type="NodeJSConfigurationType" path-to-node="wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v22.14.0/bin/node" nameIsGenerated="true" path-to-js-file="src/index.ts" typescript-loader="bundled" working-dir="$PROJECT_DIR$">
<envs>
<env name="ADDITIONAL_RESOLVERS" value="SinglePanelDevice,FourPanelDevice" />
<env name="ADDITIONAL_SCHEMAS" value="./extensions/display_devices.graphql" />
<env name="DEBUG" value="device-control-center-api:*" />
<env name="ZABBIX_AUTH_TOKEN" value="$ZABBIX_AUTH_TOKEN_VCR_DEV$" />
<env name="ZABBIX_BASE_URL" value="http://cockpit.vcr.develop.hilbigit.com/" />
<env name="ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX" value="Permissions" />
<env name="ZABBIX_ROADWORK_BASE_GROUP" value="Roadwork/Devices" />
</envs>
<EXTENSION ID="com.intellij.javascript.debugger.execution.StartBrowserRunConfigurationExtension">
<browser with-js-debugger="true" />
</EXTENSION>
<method v="2" />
</configuration>
</component>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

19
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="autoscrollToSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="openDirectoriesWithSingleClick" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;ssh.settings&quot;
}
}</component>
</project>

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
# Hint: With node_version>=21.6.0 there are problems with debugging,
# therefore the development node version is set to 21.5.0 + in order to keep dev + prod versions aligned
# this was also reflected in the Dockerfile
ARG node_version=21.5.0
#stage1
FROM node:${node_version} as builder
WORKDIR /usr/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run compile
#stage 2
FROM node:${node_version}
ARG API_VERSION
ENV API_VERSION=${API_VERSION}
WORKDIR /usr/app
COPY package*.json ./
COPY schema.graphql ./
RUN npm install --production
COPY --from=builder /usr/app/dist ./dist
CMD node dist/index.js
EXPOSE 4000

23
codegen.ts Normal file
View file

@ -0,0 +1,23 @@
import type {CodegenConfig} from '@graphql-codegen/cli';
const config: CodegenConfig = {
overwrite: true,
schema: './schema.graphql',
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript", "typescript-resolvers"],
config: {
enumValues: {
DeviceCommunicationType: "../model/model_enum_values.js#DeviceCommunicationType",
StorageItemType: "../model/model_enum_values.js#StorageItemType",
DeviceStatus: "../model/model_enum_values.js#DeviceStatus",
Permission: "../model/model_enum_values.js#Permission",
},
declarationKind: 'interface'
}
}
},
watch: true
};
export default config;

View file

@ -0,0 +1,74 @@
"""
SinglePanelDevice represents a device which can display a single picture, e.g. using LED technology.
The picture is represented either by a displaySign (a numeric value e.g. the index of the picture)
or a contentKey, which is usually the hash of the bitmap which shall be displayed.
"""
type SinglePanelDevice implements Host & Device {
hostid: ID!
"""
Per convention a uuid is used as hostname to identify devices if they do not have a unique hostname
"""
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
state: PanelState
}
type PanelState implements DeviceState {
operational: OperationalDeviceData
current: PanelCurrentState
}
type PanelCurrentState {
values: PanelValues
}
type PanelValues {
"""
Index of the bitmap which is displayed
"""
contentIndex: Int
"""
Hash of the bitmap which is displayed
"""
contentKey: String
"""
Text representation of what is displayed
"""
contentText: String
}
"""
The FourPanelDevice is a panel which allows to define pictures in 4
subpanels, called TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
"""
type FourPanelDevice implements Host & Device {
hostid: ID!
"""
Per convention a uuid is used as hostname to identify devices if they do not have a unique hostname
"""
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
state: FourPanelState
}
type FourPanelState implements DeviceState {
operational: OperationalDeviceData
current: FourPanelCurrentState
}
type FourPanelCurrentState {
values: FourPanelValues
}
type FourPanelValues {
TOP_LEFT: PanelValues
TOP_RIGHT: PanelValues
BOTTOM_LEFT: PanelValues
BOTTOM_RIGHT: PanelValues
}

24
jest.config.js Normal file
View file

@ -0,0 +1,24 @@
export default {
"roots": [
"<rootDir>/src"
],
"testMatch": [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
"setupFilesAfterEnv": ["jest-expect-message"]
}

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "zabbix-graphql-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"compile": "tsc",
"start": "nodemon --watch \"src/**\" --watch \"schema.graphql\" --ext \"ts,json\" --exec \"tsc & node --require ts-node/register --inspect --import tsx/esm ./src/index.ts\"",
"prod": "node ./dist/index.js",
"test": "jest --detectOpenHandles --forceExit --bail",
"codegen": "graphql-codegen --config codegen.ts --watch \"schema.graphql\"",
"nodemon": "nodemon --watch \"src/**\" --watch \"schema.graphql\" --ext \"ts,json\" --exec \"tsc"
},
"keywords": [],
"author": "Andreas Hilbig",
"copyright": "All rights reserved by Hilbig IT GmbH",
"dependencies": {
"@apollo/datasource-rest": "^6.3.0",
"@apollo/server": "^5.2.0",
"@as-integrations/express4": "^1.1.2",
"@graphql-tools/schema": "^10.0.30",
"class-transformer": "^0.5.1",
"cors": "^2.8.5",
"graphql": "^16.12.0",
"graphql-scalars": "^1.23.0",
"graphql-subscriptions": "^3.0.0",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.16.0",
"reflect-metadata": "^0.2.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
"@graphql-codegen/typescript": "4.0.9",
"@graphql-codegen/typescript-resolvers": "4.2.1",
"@parcel/watcher": "^2.4.1",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.6",
"@types/jest": "^29.5.13",
"@types/node": "^22.6.1",
"@types/simple-mock": "^0.8.6",
"@types/ws": "^8.5.12",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"nodemon": "^3.1.7",
"simple-mock": "^0.8.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"typescript": "^5.6.2"
}
}

625
schema.graphql Normal file
View file

@ -0,0 +1,625 @@
# Scalars resolved by package "graphql-scalars"
scalar DateTime
scalar Time
scalar JSONObject
# Schema definitions go here
type Query {
"Get api (build) version"
apiVersion: String!
"Get zabbix version"
zabbixVersion: String
"""
Login to zabbix - provided for debugging and testing purpose. The result of the login operation is
authentication token returned may be passed as
header 'zabbix-auth-token' for authenticating future API requests.
As an alternative to the cookie 'zbx_session' may be set which is automatically set after login to
the cockpit - this is the standard way to authenticate api calls initiated by the cockpit frontend
because the frontend is always embedded into the Zabbix portal which is only accessible after logging in and
obtainind the zbx_session - cookie.
"""
login(username: String!, password: String!): String
"""
Logout from zabbix - provided for debugging and testing purpose. This invalidates the token received by the login
operation. Returns true on success
"""
logout: Boolean
"""
Get all hosts + corresponding items. If with_items==true only hosts with attached items are delivered
name_pattern: If provided this will perform a LIKE "%…%" search on the name attribute within the database.
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
allHosts(name_pattern: String = "", filter_host: String = null, hostids: Int,
groupids:[Int!] = null, with_items: Boolean = false, tag_deviceType:[String]=[], tag_hostType:[String!]): [Host]
"""
Get all host groups.
If with_hosts==true only groups with attached devices are delivered.
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
allHostGroups(search_name: String, with_hosts: Boolean = true): [HostGroup]
"""
Get all locations used by hosts.
distinct_by_name=true means that the result is filtered for distinct names (default)
name_pattern: If provided this will perform a Regex search on the name attribute within the database.
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
locations(name_pattern: String = "", distinct_by_name: Boolean = true, templateids:[String] = null): [Location]
"""
Export device value history from Zabbix
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
exportDeviceValueHistory(
"(Optional) list of deviceKeys to be included in the result"
deviceKey_filter: [String!],
"(Optional) list of attribute names to be included in the result"
attribute_filter: [String!],
"""
(Optional) timestamp of earliest deviceValue"""
time_from: DateTime,
"""(Optional) timestamp of last deviceValue """
time_until: DateTime,
"""Results are sorted by timestamps - ascending or descending order may be specified
using this parameter"""
sortOrder: SortOrder=desc,
"""
Maximum number of records to be delivered. Hint: This might be useful, because the
current version of Zabbix delivers a 500 - error in case of requesting too much data
"""
limit: Int
"""
As values are stored in different data structures depending on their type
the type information must be specified in advance, although
each value (also if number) is converted into a string afterwards
"""
type: StorageItemType = FLOAT
):DeviceValueExportResponse
"""
Return all user permissions. If objectNames is provided return only the permissions related to the objects within
the objectNames - list
"""
userPermissions(objectNames: [String!]): [UserPermission!]
"""
Return true if and only if the current user (identified by token / cookie)
has all requested permissions (minimum - if READ is requested and the user has READ_WRITE
the response will be true)
"""
hasPermissions(permissions: [PermissionRequest!]!): Boolean
"""
name_pattern: If provided this will perform a LIKE "%…%" search on the name attribute within the database.
exclude_groups_pattern: Regex allowing to exclude all matching hostgroups from group permissions
"""
exportUserRights(name_pattern: String = "" exclude_hostgroups_pattern: String = ""): UserRights
}
type UserRights {
userGroups: [UserGroup!]
userRoles: [UserRole!]
}
type UserRole {
roleid: Int!
name: String
type: Int
readonly: Int
rules: UserRoleRules
}
type UserRoleRules {
ui: [UserRoleRule!]
ui_default_access: Int
modules:[UserRoleModule!]
modules_default_access: Int
api_access: Int
api_mode: Int
api: [String!]
actions: [UserRoleRule!]
actions_default_access: Int
}
type UserRoleRule {
name: String
status: Int
}
type UserRoleModule {
moduleid: String
status: Int
id: String
relative_path: String
}
type UserGroup {
usrgrpid: Int!
name: String!
gui_access: Int
users_status: Int
hostgroup_rights: [ZabbixGroupRight!]
templategroup_rights: [ZabbixGroupRight!]
}
type ZabbixGroupRight {
id: Int!
uuid: String
name: String
permission: Permission
}
########################################################
# User permissions
########################################################
input PermissionRequest {
permission: Permission!,
"""
objectName maps to name / path suffix of the template group representing the permission in Zabbix:
Permissions/{objectName}
"""
objectName: String!
}
"""
READ, READ_WRITE or DENY:
describes the rights related to an objectName
There is no EXECUTE or anything else in Zabbix,
i.e. objectName - Tree has to be designed accordingly in order to represent the perform actions.
E.g.
Let's assume a button called "button1", used in application "app1", having a label which shows "do something". Instead of model the action "do something" the idea is
to model the effect of this action - do something will result in a status change. Let's further assume that the button will only
be displayed if the user is allowed to see the current status.
Permissions/app1/button1/status
The following PermissionRequests would be used by the frontend:
1. Should the button (and its status) be displayed at all?
button1.displayed = hasPermissions(
{
objectName: "app1/button1/status"
permission: READ
})
2. Should the user be able to press the button (enabled)?
button1.displayed = hasPermissions(
{
objectName: "app1/button1/status"
permission: READ_WRITE
})
Usage Example for this pattern: Activation/Deactivation of a control program - the button
shows the possible action. If the program is active it shows "Deactivate". If the program is inactive it shows "Activate".
From this label the user learns something about the current state - therefore the status - read - permission is needed in order
to display the button at all. The status write permission is needed in order to enable the button (i.e. allow the user to press it).
in order to model the permissions to press / see a button "button1" belonging to application "app1"
the following template group could be modelled in Zabbix
"""
enum Permission {
"""
DENY superseeds anything else - i.e. if in Zabbix there is a DENY and READ at the same time the result will be DENY
"""
DENY
"""
READ superseeds READ_WRITE - i.e. if in Zabbix there is a READ_WRITE and READ at the same time the resulting permission
level will be READ
"""
READ
"""
READ_WRITE implies the READ permission. Do not set both READ and READ_WRITE at the same time if you want to achieve
READ + WRITE permission, because in this case READ will superseed the READ_WRITE and the resulting permission level will be READ
"""
READ_WRITE
}
type UserPermission {
permission: Permission!,
"""
objectName maps to name / path suffix of the template group representing the permission in Zabbix:
Permissions/{objectName}
"""
objectName: String!
}
########################################################
# Device values
########################################################
enum StorageItemType {
FLOAT
INT
TEXT
}
type DeviceValueExportResponse {
result: [JSONObject!]
error: ApiError
}
############################################################################
type Mutation {
"""
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
createHost(host: String!, hostgroupids:[Int!]!, templateids: [Int!]!,
location: LocationInput): ZabbixCreateResponse
"""
(Mass) Import zabbix groups
and assign them to the corresponding hosts by groupid or groupName.
Return value: If no error occurs a groupid be returned for each created group,
otherwise the return object will contain an error message
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
importHostGroups(hostGroups: [CreateHostGroup!]!):[CreateHostGroupResponse!]
"""
(Mass) Import edge devices and assign them to host groups by groupid or groupName.
Return value: If no error occurs a hostid will be returned for each created device,
otherwise the return object will contain an error message. The hostid will be equal
to the provided deviceKey if the operation was successfull.
Authentication: By zbx_session - cookie or zabbix-auth-token - header
"""
importHosts(devices: [CreateHost!]!):[CreateHostResponse!]
importUserRights(input: UserRightsInput!, dryRun: Boolean! = true): ImportUserRightsResult
}
input UserRightsInput {
userRoles: [UserRoleInput!]
userGroups: [UserGroupInput!]
}
input UserRoleInput {
name: String
type: Int
readonly: Int
rules: UserRoleRulesInput
}
input UserRoleRulesInput {
ui: [UserRoleRuleInput!]
ui_default_access: Int
modules:[UserRoleModuleInput!]
modules_default_access: Int
api_access: Int
api_mode: Int
api: [String!]
actions: [UserRoleRuleInput!]
actions_default_access: Int
}
input UserRoleRuleInput {
name: String
status: Int
}
input UserRoleModuleInput {
moduleid: String
status: Int
id: String
}
input UserGroupInput {
name: String!
gui_access: Int
users_status: Int
hostgroup_rights: [ZabbixGroupRightInput!]
templategroup_rights: [ZabbixGroupRightInput!]
}
input ZabbixGroupRightInput {
uuid: String
"""
name may optionally be specified for documentation purpose,
but the master for setting the user right is the uuid.
If a uuid is found and the corresponding group
has a deviating name this will be documented within a message
with errorcode = 0 (OK) but the permission will be set (
the reason is that names for groups may deviate between several
instances of the control center although the semantic is the same -
while the semantic is identified by uuid.
"""
name: String
permission: Permission
}
type ImportUserRightsResult {
userRoles: [ImportUserRightResult!]
userGroups: [ImportUserRightResult!]
}
type ImportUserRightResult {
id: String
name: String
message: String
errors: [ApiError!]
}
"""
Hint: WGS84[dd.ddddd] coordinates are used
"""
interface GpsPosition {
latitude: Float
longitude: Float
}
#########################################
type HostGroup {
groupid: ID!
name: String
}
interface Host {
hostid: ID!
"""
The host field contains the "hostname" in Zabbix
"""
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
}
type ZabbixItem {
itemid: Int!
name: String!
key_: String!
hostid: Int
lastclock: Int
lastvalue: String
value_type: Int!
attributeName: String
deviceType: String
topicType:String
status: DeviceStatus
type: DeviceCommunicationType
hosts: [Host]
}
type ZabbixHost implements Host {
hostid: ID!
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
items: [ZabbixItem!]
inventory: Inventory
parentTemplates: [Template!]
}
"""
(IoT / Edge - ) Devices are hosts having a state containing the "output" / the business data which is exposed
besides monitoring information.
"""
interface Device implements Host {
hostid: ID!
"""
Per convention a uuid is used as hostname to identify devices if they do not have a unique hostname
"""
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
state: DeviceState
}
type OperationalDeviceData {
temperature: Float
voltage: Float
signalstrength: Float
location: Location
timestamp: DateTime
error: [ErrorPayload!]
}
interface DeviceState {
operational: OperationalDeviceData
}
# Generic IoT devices with "generic" current state - mapping all "values"
type GenericDeviceState implements DeviceState {
operational: OperationalDeviceData
current: JSONObject
}
"""
Device represents generic IoT / Edge - devices providing their state as generic "state.current" - JSON Object
"""
type GenericDevice implements Host & Device {
hostid: ID!
"""
Per convention a uuid is used as hostname to identify devices if they do not have a unique hostname
"""
host: String!
deviceType: String
hostgroups: [HostGroup!]
name: String
tags: JSONObject
state: GenericDeviceState
}
type ErrorPayload {
code: Int!
message: String
additionalInfo: JSONObject
}
enum SensorValueType {
NUMERIC # 0 - numeric float;
CHARACTER # 1 - character;
LOG # 2 - log;
NUMERIC_UNSIGNED # 3 - numeric unsigned;
TEXT # 4 - text;
}
type ZabbixCreateResponse {
hostids: [Int]
itemids: [Int]
error: ApiError
}
input LocationInput {
name: String
location_lat: String
location_lon: String
}
type Template {
templateid: String!
name: String
}
type Inventory {
location: Location
}
type Location implements GpsPosition {
name: String
latitude: Float
longitude: Float
}
enum DeviceStatus {
ENABLED
DISABLED
}
enum DeviceCommunicationType {
ZABBIX_AGENT
ZABBIX_AGENT_ACTIVE
ZABBIX_TRAP
ZABBIX_INTERNAL_ITEM
SIMPLE_CHECK
DEPENDANT_ITEM
SIMULATOR_CALCULATED
SIMULATOR_JAVASCRIPT
HTTP_AGENT
IPMI_AGENT
JMX_AGENT
SNMP_AGENT
SNMP_TRAP
DATABASE_MONITOR
}
type Item {
itemid: Int
hostid: Int
name: String!
key_: String
attributeName: String
deviceType: String
topicType:String
status: DeviceStatus
type: DeviceCommunicationType
hosts: [Host]
}
####################################################################
# Input types used for importXXX - and storeXXX - Mutations
####################################################################
input CreateHostGroup {
"""
Name of the host group
"""
groupName: String!
"""
Internally used unique id
(will be assigned by Zabbix if empty)
"""
uuid: String
}
type CreateHostGroupResponse {
groupName: String!
groupid: Int
message: String
error: ApiError
}
type HostTypeMeta {
deviceType: String
deviceTypeDescription: String
}
input CreateHost {
deviceKey: String!
"""
Optional display name of the device (must be unique if provided - default is to set display name to deviceKey)
"""
name: String
deviceType: String!
"""
groupNames is used to assign the created object
to a host group. It is mandatory but
can also be blank. This is usefull in case of
passing a groupid instead which is
the zabbix internal key for storing the group.
If a groupid is provided the passed groupName is ignored
"""
groupNames: [String!]!
"""
Optionally the internal groupids can be passed - in this case the
groupName is ignored
"""
groupids: [Int]
location: LocationInput
}
type CreateHostResponse {
deviceKey: String!
hostid: String
message: String
error: ApiError
}
interface Error {
code: Int
message: String
data: JSONObject
}
type ApiError implements Error {
code: Int
message: String
data: JSONObject
path: String
args: JSONObject
}
############################################################################
# General purpose types + enums
############################################################################
enum SortOrder {
"Deliver values in ascending order"
asc
"Deliver values in descending order"
desc
}

194
src/api/resolver_helpers.ts Normal file
View file

@ -0,0 +1,194 @@
import {isObjectType} from "graphql";
import {logger} from "../logging/logger.js";
/*
As a default all . - seperators within a key shall be replaced by a Capital letter of the following word
*/
function defaultKeyMappingFunction(key: string): string {
let words = key.split(".")
for (let i = 1; i < words.length; i++) {
if (words[i]) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
}
return words.join("")
}
export function createHierarchicalValueFieldResolver(
schema: any, typename: string,
sourceFieldMapper: (fieldname: string, parent: any, objectTypeRequested: boolean) => { [p: string]: any } | null): {
[fieldname: string]: any
} {
let resolver: { [fieldname: string]: any } = {}
let type = schema.getType(typename)
if (isObjectType(type)) {
let fields = type.getFields();
for (let fieldsKey in fields) {
let field = fields[fieldsKey];
resolver[field.name] = (parent: any) => sourceFieldMapper(field.name, parent, isObjectType(field.type));
}
}
return resolver
}
export function zabbixItemValueSourceFieldMapper(
fieldname: string,
parent: {
items: [{ itemid: string, key_: string; name: string, lastvalue: any }],
[key: string]: any
},
objectTypeRequested: boolean
) {
let result: { [p: string]: any; } | any = parent[fieldname]
if (!parent.items) {
logger.debug(`No parent.items found: ${JSON.stringify(parent)}`)
return result
}
parent.items.forEach(
item => {
result = mapAttributeListToGraphQlType(result, objectTypeRequested, fieldname, {
key: item.key_,
value: item.lastvalue
}
)
}
)
logger.info(`Device data mapped: ${JSON.stringify(result)}`)
return result;
}
export function zabbixTagsValueSourceFieldMapper(
fieldname: string,
tags: [{ tag: string, value: any }],
objectTypeRequested: boolean
) {
let result: { [p: string]: any; } | any = {}
if (!tags) {
logger.debug(`No parent.tags or parent.inheritedTags found: ${JSON.stringify(tags)}`)
return result
}
tags.forEach(
tag => {
result = mapAttributeListToGraphQlType(result, objectTypeRequested, fieldname, {
key: tag.tag,
value: tag.value
}
)
}
)
logger.info(`Device tags mapped: ${JSON.stringify(result)}`)
return result;
}
function mapAttributeListToGraphQlType(result: {
[p: string]: any;
} | any, objectTypeRequested: boolean, fieldname: string, item: { value: any, key: string }) {
logger.debug(`Resolving ${objectTypeRequested ? "attributes of object" : "value of scalar"} field parent.${fieldname} (${result}), looking up key from item ${JSON.stringify(item)}`)
if (item.key) {
if (objectTypeRequested) {
function addRecursive(
result: { [x: string]: any; } | null,
fieldHierarchy: string[],
value: any
) {
if (!fieldHierarchy || fieldHierarchy.length == 0) {
return result
} else {
if (!result) {
result = {}
}
if (fieldHierarchy.length == 1) {
let fieldTokenName = fieldHierarchy[0];
const TOKEN_SEPERATOR = "_";
// As value is not typed we must parse the type in order to transform it to a strongly
// typed value which is expected by Graphql
// Example: Graphql does not accept a string "true" and empty string as false as boolean
// In order to facilitate this it is possible (but not mandatory) to provide typehints
// to item keys by prepending the fieldTokenName with a typehint, following by an underscore.
// I.e. if a key is prefixed with str_, bool_, float_ or json_ this will be stripped
// and the value will be cast to the appropriate type.
// If no typeHint is provided and the type is string it will be tried to create a float
// or a boolean out of it.
let typeHintToken = fieldTokenName.split(TOKEN_SEPERATOR);
let typeHint = undefined;
if (typeHintToken.length > 0) {
switch (typeHintToken[0]) {
case "str":
case "bool":
case "float":
case "json":
typeHint = typeHintToken[0];
// Remove typehint + token separator from field name - if the typehint
// is followed by another token. If not (e.g. fieldTokenName="str" only) the
// token is considered to be a valid typehint, but it is not stripped from the
// fieldTokenName (i.e. nothing happens to the fieldTokenName in that case)
if (typeHintToken.length > 1) {
fieldTokenName = fieldTokenName.substring(typeHint.length + 1);
}
}
}
let fieldValue = undefined;
if (typeof value === 'string' && (typeHint == "bool" || value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
fieldValue = (value.toLowerCase() === 'true');
// logger.debug("Parsing attribute '" + fieldTokenName + "' as true/false string, type=" + typeof fieldValue + ", value=" + fieldValue);
} else if (typeof value === 'string' && value !== '' && !isNaN(Number(value))) {
fieldValue = Number(value);
// logger.debug("Parsing attribute '" + fieldTokenName + "' as number, type=" + typeof fieldValue + ", value=" + fieldValue);
} else if (typeof value === 'string' && typeHint == "json") {
logger.debug("Trying to parse attribute value as json, typeHint=" + typeHint + ", type=" + typeof value + ", value=" + value);
if (!value) {
// Empty string values will be considered to be an unset JSON-Object if there is a typeHin=="json"
fieldValue = undefined;
} else {
try {
fieldValue = JSON.parse(value);
// logger.debug("Parsing attribute '" + fieldTokenName + "' as json, type=" + typeof fieldValue + ", value=" + value);
} catch (e) {
logger.debug("Unable to parse attribute value as json, passing unmodified, type=" + typeof value + ", value=" + value);
fieldValue = value;
}
}
} else {
fieldValue = value;
// logger.debug("Passing attribute '" + fieldTokenName + "' unmodified, type=" + typeof fieldValue + ", value=" + fieldValue);
}
if (fieldValue !== undefined) {
// logger.debug(`length of parsed field hierarchy is 1: Setting fieldTokenName=${fieldTokenName} to ${value}`);
result[fieldTokenName] = fieldValue;
} /*else {
// logger.debug(`length of parsed field hierarchy is 1: Skipping to set fieldHierarchy=${fieldHierarchy} to ${value} (empty value)`);
}*/
} else {
result[fieldHierarchy[0]] = addRecursive(result[fieldHierarchy[0]], fieldHierarchy.slice(1), value)
}
}
logger.debug(`Adding attribute ${fieldHierarchy[0]}, result: ${JSON.stringify(result)}`)
return result
}
let fieldHierarchy = item.key.split(".");
if (fieldHierarchy[0] == fieldname) {
result = addRecursive(result, fieldHierarchy.slice(1), item.value);
logger.debug(`Detected matching item key ${fieldname} in item , result: ${JSON.stringify(result)}`)
} else {
logger.debug(`Item key ${fieldHierarchy[0]} not matched fieldname=${fieldname}, result: ${JSON.stringify(result)}`)
}
} else {
let keyInCamel = defaultKeyMappingFunction(item.key);
if (keyInCamel == fieldname) {
result = item.value
logger.debug(`Detected matching item key ${keyInCamel} in item , result: ${JSON.stringify(result)}`)
}
}
}
return result;
}

257
src/api/resolvers.ts Normal file
View file

@ -0,0 +1,257 @@
import {
DeviceCommunicationType,
DeviceStatus,
MutationCreateHostArgs,
MutationImportHostsArgs,
MutationImportHostGroupsArgs,
MutationImportUserRightsArgs,
Permission,
QueryAllHostsArgs,
QueryAllHostGroupsArgs,
QueryExportDeviceValueHistoryArgs,
QueryExportUserRightsArgs,
QueryHasPermissionsArgs,
QueryUserPermissionsArgs,
Resolvers,
StorageItemType, Host,
} from "../generated/graphql.js";
import {HostImporter} from "../execution/host_importer";
import {HostValueExporter} from "../execution/host_exporter";
import {logger} from "../logging/logger.js";
import {ParsedArgs, ZabbixPermissionsHelper, ZabbixRequest} from "../datasources/zabbix-request.js";
import {ZabbixCreateHostRequest, ZabbixQueryHostsRequestWithItemsAndInventory,} from "../datasources/zabbix-hosts.js";
import {ZabbixQueryHostgroupsParams, ZabbixQueryHostgroupsRequest} from "../datasources/zabbix-hostgroups.js";
import {
ZabbixExportUserGroupArgs,
ZabbixExportUserGroupsRequest,
ZabbixImportUserGroupsParams,
ZabbixImportUserGroupsRequest
} from "../datasources/zabbix-usergroups.js";
import {
ZabbixImportUserRolesParams,
ZabbixImportUserRolesRequest,
ZabbixQueryUserRolesRequest
} from "../datasources/zabbix-userroles.js";
import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api";
import {GraphQLInterfaceType, GraphQLList} from "graphql/type";
export function createResolvers(): Resolvers {
// @ts-ignore
// @ts-ignore
return {
Query: {
userPermissions: async (_parent: any, objectNamesFilter: QueryUserPermissionsArgs, {
zabbixAuthToken,
cookie
}: any) => {
return ZabbixPermissionsHelper.getUserPermissions(zabbixAPI, zabbixAuthToken, cookie, objectNamesFilter.objectNames)
},
hasPermissions: async (_parent: any, args: QueryHasPermissionsArgs, {zabbixAuthToken, cookie}: any) => {
return ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, args, zabbixAuthToken, cookie)
},
locations: (_parent: any, args: Object, {dataSources, zabbixAuthToken}: any) => {
return dataSources.zabbixAPI.getLocations(zabbixAuthToken, new ParsedArgs(args));
},
apiVersion: () => {
return process.env.API_VERSION ?? "unknown"
},
zabbixVersion: async () => {
return await new ZabbixRequest<string>("apiinfo.version").executeRequestThrowError(
zabbixAPI)
},
login: async (_parent, args) => {
return await new ZabbixRequest<any>("user.login").executeRequestThrowError(
zabbixAPI, new ParsedArgs(args))
},
logout: async (_parent, _args, {zabbixAuthToken, cookie}: any) => {
return await new ZabbixRequest<any>("user.logout", undefined, cookie).executeRequestThrowError(zabbixAPI);
},
allHosts: async (_parent: any, args: QueryAllHostsArgs, {
zabbixAuthToken,
cookie, dataSources
}: any) => {
args.tag_hostType ??= [ZABBIX_EDGE_DEVICE_BASE_GROUP];
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
.executeRequestThrowError(
dataSources.zabbixAPI, new ParsedArgs(args)
)
},
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
zabbixAuthToken,
cookie
}: any) => {
if (!args.search_name) {
args.search_name = ZABBIX_EDGE_DEVICE_BASE_GROUP + "/*"
}
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
zabbixAPI, new ZabbixQueryHostgroupsParams(args)
)
},
exportDeviceValueHistory: (_parent: any, args: QueryExportDeviceValueHistoryArgs, {
zabbixAuthToken,
cookie
}: any) => {
return HostValueExporter.exportDeviceData(args, zabbixAuthToken, cookie)
},
exportUserRights: async (_, args: QueryExportUserRightsArgs, {
zabbixAuthToken,
cookie
}: any) => {
let groups = await new ZabbixExportUserGroupsRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ZabbixExportUserGroupArgs(args.name_pattern, args.exclude_hostgroups_pattern));
let roles = await new ZabbixQueryUserRolesRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ParsedArgs(args.name_pattern ? {name_pattern: args.name_pattern} : undefined));
return {
userGroups: groups,
userRoles: roles
}
}
},
Mutation: {
createHost: async (_parent: any, args: MutationCreateHostArgs, {
zabbixAuthToken,
cookie
}: any) => {
return await new ZabbixCreateHostRequest(zabbixAuthToken, cookie).executeRequestThrowError(
zabbixAPI,
new ParsedArgs(args)
)
},
importHostGroups: async (_parent: any, args: MutationImportHostGroupsArgs, {
zabbixAuthToken,
cookie
}: any) => {
return HostImporter.importHostGroups(args.hostGroups, zabbixAuthToken, cookie)
},
importHosts: async (_parent: any, args: MutationImportHostsArgs, {
zabbixAuthToken,
cookie
}: any) => {
return HostImporter.importHosts(args.devices, zabbixAuthToken, cookie)
},
importUserRights: async (_, args: MutationImportUserRightsArgs, {
zabbixAuthToken,
cookie
}: any) => {
let userRoleImportArgs = structuredClone(args);
let userGroupImportArgs = structuredClone(args);
let userRolesImport = userRoleImportArgs.input.userRoles ?
await new ZabbixImportUserRolesRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI,
new ZabbixImportUserRolesParams(userRoleImportArgs.input.userRoles, userRoleImportArgs.dryRun)) : null;
let userGroupsImport = userGroupImportArgs.input.userGroups ?
await new ZabbixImportUserGroupsRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI,
new ZabbixImportUserGroupsParams(userGroupImportArgs.input.userGroups, userGroupImportArgs.dryRun)) : null;
return {
userRoles: userRolesImport,
userGroups: userGroupsImport
}
}
},
Host: {
// @ts-ignore
__resolveType: function (host: Host, _context, info ): string {
const deviceType = host.deviceType ?? "";
if (deviceType) {
logger.info(`checking host ${host.name} for deviceType - found ${deviceType}`);
let interfaceType: GraphQLInterfaceType = (info.returnType instanceof GraphQLList ?
info.returnType.ofType : info.returnType) as GraphQLInterfaceType
if (info.schema.getImplementations(interfaceType).objects.some((impl: { name: string; }) => impl.name === deviceType)) {
return deviceType;
}
return "GenericDevice"
}
logger.info(`checking host ${host.name} for deviceType - no device type found, returning as ZabbixHost`);
return "ZabbixHost"; // Return "generic" device host as a default if no templates are assigned
}
},
Inventory: {
/*
Always map inventory.location,... fields to location object
*/
// @ts-ignore
location: (parent: { location: string; location_lon: string; location_lat: string; }) => {
return {
name: parent.location,
longitude: parent.location_lon,
latitude: parent.location_lat,
}
}
},
UserRoleRules: {
ui_default_access: (parent: any) => {
return parent["ui.default_access"]
},
modules_default_access: (parent: any) => {
return parent["modules.default_access"]
},
actions_default_access: (parent: any) => {
return parent["actions.default_access"]
},
api_access: (parent: any) => {
return parent["api.access"]
},
api_mode: (parent: any) => {
return parent["api.mode"]
},
},
// Enum Value Mappings
Permission: {
READ: Permission.Read,
READ_WRITE: Permission.ReadWrite,
DENY: Permission.Deny
},
DeviceCommunicationType: {
ZABBIX_AGENT: DeviceCommunicationType.ZABBIX_AGENT,
ZABBIX_AGENT_ACTIVE: DeviceCommunicationType.ZABBIX_AGENT_ACTIVE,
ZABBIX_TRAP: DeviceCommunicationType.ZABBIX_TRAP,
SIMPLE_CHECK: DeviceCommunicationType.SIMPLE_CHECK,
ZABBIX_INTERNAL_ITEM: DeviceCommunicationType.ZABBIX_INTERNAL_ITEM,
DEPENDANT_ITEM: DeviceCommunicationType.DEPENDANT_ITEM,
HTTP_AGENT: DeviceCommunicationType.HTTP_AGENT,
SIMULATOR_CALCULATED: DeviceCommunicationType.SIMULATOR_CALCULATED,
SNMP_AGENT: DeviceCommunicationType.SNMP_AGENT,
SNMP_TRAP: DeviceCommunicationType.SNMP_TRAP,
IPMI_AGENT: DeviceCommunicationType.IPMI_AGENT,
JMX_AGENT: DeviceCommunicationType.JMX_AGENT,
SIMULATOR_JAVASCRIPT: DeviceCommunicationType.SIMULATOR_JAVASCRIPT,
DATABASE_MONITOR: DeviceCommunicationType.DATABASE_MONITOR,
},
DeviceStatus: {
ENABLED: DeviceStatus.ENABLED,
DISABLED: DeviceStatus.DISABLED
},
SensorValueType: {
NUMERIC: 0,
CHARACTER: 1,
LOG: 2,
NUMERIC_UNSIGNED: 3,
TEXT: 4
},
StorageItemType: {
TEXT: StorageItemType.Text,
FLOAT: StorageItemType.Float,
INT: StorageItemType.Int,
}
}
}

59
src/api/schema.ts Normal file
View file

@ -0,0 +1,59 @@
import {
createHierarchicalValueFieldResolver,
zabbixItemValueSourceFieldMapper,
zabbixTagsValueSourceFieldMapper
} from "./resolver_helpers.js";
import {makeExecutableSchema, mergeSchemas} from "@graphql-tools/schema";
import {readFileSync} from "fs";
import {GraphQLSchema} from "graphql/type";
import {createResolvers} from "./resolvers.js";
const createZabbixHierarchicalDeviceFieldResolver =
(typename: string, schema: any, additionalMappings: { [p: string]: any } = {}) => {
return {
...createHierarchicalValueFieldResolver(schema, typename, zabbixItemValueSourceFieldMapper),
...additionalMappings
}
}
const createZabbixHierarchicalDeviceTagsResolver =
(typename: string, schema: any, additionalMappings: { [p: string]: any } = {}) => {
return {
...createHierarchicalValueFieldResolver(schema, typename, zabbixTagsValueSourceFieldMapper),
...additionalMappings
}
}
export async function schema_loader(): Promise<GraphQLSchema> {
const resolvers = createResolvers();
let typeDefs: string = readFileSync('./schema.graphql', {encoding: 'utf-8'});
if (process.env.ADDITIONAL_SCHEMAS) {
for (const schema of process.env.ADDITIONAL_SCHEMAS.split(",")){
typeDefs += readFileSync(schema, {encoding: 'utf-8'});
}
}
let originalSchema =
makeExecutableSchema({
typeDefs,
resolvers,
});
let additionalMappings = {
tags: (parent: { tags: any; inheritedTags: any }) => {
return (parent.tags || []).concat(parent.inheritedTags || [])
}
}
let genericResolvers: Record<string, any> = {
Device: createZabbixHierarchicalDeviceFieldResolver("Device", originalSchema,additionalMappings ),
GenericDevice: createZabbixHierarchicalDeviceFieldResolver("GenericDevice", originalSchema, additionalMappings),
}
if (process.env.ADDITIONAL_RESOLVERS) {
for (const resolver of process.env.ADDITIONAL_RESOLVERS.split(",")){
genericResolvers[resolver] = createZabbixHierarchicalDeviceFieldResolver(resolver, originalSchema, additionalMappings)
}
}
return mergeSchemas({
schemas: [originalSchema],
// TODO Generate resolvers for all schema types with @generateZabbix directive automatically
resolvers: genericResolvers
});
}

98
src/api/start.ts Normal file
View file

@ -0,0 +1,98 @@
import http from "http";
import {schema_loader} from "./schema.js";
import {GraphQLSchema} from "graphql/type";
import {ApolloServer} from "@apollo/server";
import {expressMiddleware} from '@as-integrations/express4';
import express from 'express';
import cors from "cors";
import {ApolloServerPluginDrainHttpServer} from '@apollo/server/plugin/drainHttpServer';
import {logger} from "../logging/logger.js";
import {zabbixAPI, zabbixRequestAuthToken} from "../datasources/zabbix-api";
import {WebSocketServer} from "ws";
import {useServer} from "graphql-ws/lib/use/ws";
const GRAPHQL_PATH = "/"
const GRAPHQL_PORT = 4000
export function startAPi() {
startApolloServer().then(
r => {
logger.info(`🚀 API ready at http://localhost:` + GRAPHQL_PORT + GRAPHQL_PATH);
});
}
async function startApolloServer() {
return schema_loader().then(async (executableSchema: GraphQLSchema) => {
// Required logic for integrating with Express
const app = express();
// Our httpServer handles incoming requests to our Express app.
// Below, we tell Apollo Server to "drain" this httpServer,
// enabling our servers to shut down gracefully.
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
// This is the `httpServer` we created in a previous step.
server: httpServer,
// Pass a different path here if app.use
// serves expressMiddleware at a different path
path: GRAPHQL_PATH,
});
// Hand in the schema we just created and have the
// WebSocketServer start listening.
const serverCleanup = useServer({schema: executableSchema}, wsServer);
const server: ApolloServer = new ApolloServer({
schema: executableSchema,
plugins: [
// Proper shutdown for the HTTP server.
ApolloServerPluginDrainHttpServer({httpServer}),
// Proper shutdown for the WebSocket server.
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
// Set up our Express middleware to handle CORS, body parsing,
// and our expressMiddleware function.
app.use(
GRAPHQL_PATH,
cors<cors.CorsRequest>(),
express.json(),
// expressMiddleware accepts the same arguments:
// an Apollo Server instance and optional configuration options
expressMiddleware(server, {
context: async ({req}) => {
const {cache} = server;
return {
cache,
dataSources: {
zabbixAPI: zabbixAPI,
},
zabbixAuthToken: req.headers["zabbix-auth-token"] ?? zabbixRequestAuthToken,
cookie: req.headers.cookie,
token: req.headers.token
};
},
}),
);
// Modified server startup
await new Promise<void>((resolve) => httpServer.listen({port: GRAPHQL_PORT}, resolve));
});
}

8
src/common_utils.ts Normal file
View file

@ -0,0 +1,8 @@
export function sleep(ms: number): { promise: Promise<void>, cancel: () => void } {
let timeoutId: NodeJS.Timeout;
const promise = new Promise<void>((resolve) => {
timeoutId = setTimeout(resolve, ms);
});
const cancel = () => clearTimeout(timeoutId);
return { promise, cancel };
}

View file

@ -0,0 +1,115 @@
import {
CacheOptions,
DataSourceConfig,
DataSourceFetchResult,
DataSourceRequest,
PostRequest,
RESTDataSource
} from "@apollo/datasource-rest";
import {logger} from "../logging/logger.js";
import {ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
export const zabbixRequestAuthToken = process.env.ZABBIX_AUTH_TOKEN_FOR_REQUESTS
export const zabbixSuperAuthToken = process.env.ZABBIX_AUTH_TOKEN
export const ZABBIX_EDGE_DEVICE_BASE_GROUP = process.env.ZABBIX_EDGE_DEVICE_BASE_GROUP || process.env.ZABBIX_ROADWORK_BASE_GROUP || "Baustellen-Devices"
export const FIND_ZABBIX_EDGE_DEVICE_BASE_GROUP_PREFIX = new RegExp(`^(${ZABBIX_EDGE_DEVICE_BASE_GROUP})\/`)
export class ZabbixAPI
extends RESTDataSource {
private static readonly MAX_LOG_REQUEST_BODY_LIMIT_LENGTH = 500
constructor(public baseURL: string, config?: DataSourceConfig) {
super(config);
logger.info("Connecting to Zabbix at url=" + this.baseURL)
}
override async fetch<Object>(path: string, incomingRequest: DataSourceRequest = {}): Promise<DataSourceFetchResult<Object>> {
logger.debug(`Zabbix request path=${path}, body=${JSON.stringify(incomingRequest.body).substring(0, ZabbixAPI.MAX_LOG_REQUEST_BODY_LIMIT_LENGTH)} (...)`)
let response_promise_original
try {
const response_promise: Promise<DataSourceFetchResult<Object>> = super.fetch("api_jsonrpc.php", incomingRequest);
try {
const response = await response_promise;
const body = response.parsedBody;
return await new Promise!<DataSourceFetchResult<Object>>((resolve, reject) => {
if (body && body.hasOwnProperty("result")) {
// @ts-ignore
let result: any = body["result"];
response.parsedBody = result;
if (result) {
logger.debug(`Found and returned result - length = ${result.length}`);
if (!Array.isArray(result) || !result.length) {
logger.debug(`Result: ${JSON.stringify(result)}`);
} else {
result.forEach((entry: any) => {
if (entry.hasOwnProperty("tags")) {
entry["tags"].forEach((tag: { tag: string; value: string; }) => {
entry[tag.tag] = tag.value;
});
}
if (entry.hasOwnProperty("inheritedTags")) {
entry["inheritedTags"].forEach((tag_1: { tag: string; value: string; }) => {
entry[tag_1.tag] = tag_1.value;
});
}
});
}
}
resolve(response);
} else {
let error_result: any;
if (body && body.hasOwnProperty("error")) {
// @ts-ignore
error_result = body["error"];
} else {
error_result = body;
}
logger.error(`No result for Zabbix request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(error_result)}`);
resolve(response);
}
});
} catch (reason) {
let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(reason)}`;
logger.error(msg);
return response_promise
}
} catch (e) {
let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(e)}`
logger.error(msg)
// @ts-ignore
return response_promise_original
}
}
public post<TResult = any>(path: string, request?: PostRequest<CacheOptions>): Promise<TResult> {
return super.post(path, request);
}
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true): Promise<T | ZabbixErrorResult> {
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args);
}
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true) {
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError);
}
async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) {
const hosts_promise = this.requestByPath("host.get", args, authToken, cookies);
return hosts_promise.then(response => {
// @ts-ignore
let locations = response.filter((host) => host.hasOwnProperty("inventory")).map(({inventory: x}) => x);
if (args?.distinct_by_name || args?.name_pattern) {
locations = locations.filter((loc: { location: string; }, i: number, arr: any[]) => {
return loc.location && (!args.distinct_by_name || arr.indexOf(arr.find(t => t.location === loc.location)) === i)
&& (!args.name_pattern || new RegExp(args.name_pattern).test(loc.location));
});
}
return locations;
});
}
}
export const zabbixAPI = new ZabbixAPI(process.env.ZABBIX_BASE_URL || "")

View file

@ -0,0 +1,128 @@
import {ZabbixAPI} from "./zabbix-api.js";
import {ApiError, SortOrder, StorageItemType} from "../generated/graphql.js";
import {ZabbixCreateOrUpdateStorageItemRequest} from "./zabbix-items.js";
import {ZabbixForceCacheReloadRequest} from "./zabbix-script.js";
import {logger} from "../logging/logger.js";
import {ApiErrorCode} from "../model/model_enum_values.js";
import {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
import {sleep} from "../common_utils";
export interface ZabbixValue {
key?: string,
host?: string,
value: string,
clock: number,
ns: number
}
export interface ZabbixExportValue extends ZabbixValue, ZabbixResult {
itemid?: string
}
export class ZabbixHistoryGetParams extends ParsedArgs {
time_from_ms: number | undefined
time_till_ms: number | undefined
constructor(public itemids: number[] | number | string | string[],
public output: string[] = ["value", "itemid", "clock", "ns"],
public limit: number | null = Array.isArray(itemids) ? itemids.length : 1,
public history: StorageItemType | string = StorageItemType.Text,
time_from?: Date,
time_until?: Date,
public sortfield: string[] = ["clock", "ns"],
public sortorder: SortOrder | null = SortOrder.Desc,
) {
super();
this.time_from_ms = time_from ? Math.floor(new Date(time_from).getTime() / 1000) : undefined
this.time_till_ms = time_until ? Math.floor(new Date(time_until).getTime() / 1000) : undefined
}
}
export class ZabbixQueryHistoryRequest extends ZabbixRequest<ZabbixExportValue[], ZabbixHistoryGetParams> {
constructor(authToken?: string | null, cookie?: string | null) {
super("history.get", authToken, cookie);
}
createZabbixParams(args?: ZabbixHistoryGetParams): ZabbixParams {
return {
itemids: args?.itemids,
output: args?.output,
limit: args?.limit,
history: args?.history?.valueOf(),
sortfield: args?.sortfield,
sortorder: args?.sortorder == SortOrder.Asc ? "ASC" : "DESC",
time_from: args?.time_from_ms,
time_till: args?.time_till_ms,
}
}
}
export interface ZabbixHistoryPushResult {
response: string,
data: { itemid: string, error?: string[] | ApiError }[],
error?: ApiError | string[]
}
export class ZabbixHistoryPushRequest extends ZabbixRequest<ZabbixHistoryPushResult> {
constructor(authToken?: string | null, cookie?: string) {
super("history.push", authToken, cookie);
}
}
export class ZabbixStoreObjectInItemHistoryRequest extends ZabbixRequest<ZabbixHistoryPushResult> {
// After creating an item or host zabbix needs some time before the created object can be referenced in other
// operations - the reason is the config-cache. In case of having ZBX_CACHEUPDATEFREQUENCY=1 (seconds) set within the
// Zabbix - config the delay of 1 second will be sufficient
private static readonly ZABBIX_DELAY_UNTIL_CONFIG_CHANGED: number = 0
public itemid: number | undefined
constructor(authToken?: string | null, cookie?: string) {
super("history.push.jsonobject", authToken, cookie);
}
async prepare(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<any> {
// Create or update zabbix Item
let success = false;
this.itemid = Number(args?.getParam("itemid"))
let timeoutForValueUpdate = this.itemid ? 0 : ZabbixStoreObjectInItemHistoryRequest.ZABBIX_DELAY_UNTIL_CONFIG_CHANGED;
// Create or update controlprogram - item
let result: {
"itemids": string[]
} | undefined = await new ZabbixCreateOrUpdateStorageItemRequest(
this.itemid ? "item.update.storeiteminhistory" : "item.create.storeiteminhistory",
this.authToken, this.cookie).executeRequestThrowError(zabbixAPI, args)
// logger.debug(`Create/update item itemid=${this.itemid}, hostid=${this.zabbixHostId} lead to result=`, JSON.stringify(result));
if (result && result.hasOwnProperty("itemids") && result.itemids.length > 0) {
this.itemid = Number(result.itemids[0]);
let scriptExecResult =
await new ZabbixForceCacheReloadRequest(this.authToken, this.cookie).executeRequestThrowError(zabbixAPI)
if (scriptExecResult.response != "success") {
logger.error(`cache reload not successful: ${scriptExecResult.value}`)
}
await sleep(timeoutForValueUpdate).promise
}
if (!this.itemid) {
this.prepResult = {
error: {
message: "Unable to create/update item",
code: ApiErrorCode.ZABBIX_NO_ITEM_PUSH_ITEM,
path: this.path,
args: args,
}
}
}
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
itemid: this.itemid,
value: JSON.stringify(args?.getParam("value"))
}
}
}

View file

@ -0,0 +1,119 @@
import {isZabbixErrorResult, ParsedArgs, ZabbixParams, ZabbixRequest} from "./zabbix-request.js";
import {Permission} from "../generated/graphql.js";
import {
FIND_ZABBIX_EDGE_DEVICE_BASE_GROUP_PREFIX,
ZABBIX_EDGE_DEVICE_BASE_GROUP,
ZabbixAPI,
zabbixSuperAuthToken
} from "./zabbix-api";
import {logger} from "../logging/logger";
export interface CreateHostGroupResult {
groupids: string[]
}
const hostGroupReadWritePermissions = {
permissions: [
{
objectName: "Hostgroup/ConstructionSite",
permission: Permission.ReadWrite
}]
}
const hostGroupReadPermissions = {
permissions: [
{
objectName: "Hostgroup/ConstructionSite",
permission: Permission.Read
}]
}
export class ZabbixCreateHostGroupRequest extends ZabbixRequest<CreateHostGroupResult> {
constructor(_authToken?: string | null, cookie?: string) {
super("hostgroup.create", zabbixSuperAuthToken, cookie, hostGroupReadWritePermissions);
}
}
export class ZabbixDeleteHostGroupRequest extends ZabbixRequest<{
"groupids": string []
}> {
constructor(_authToken?: string | null, cookie?: string) {
super("hostgroup.delete", zabbixSuperAuthToken, cookie, {
permissions: [
{
objectName: "Hostgroup/ConstructionSite",
permission: Permission.ReadWrite
}]
});
}
}
export class ZabbixQueryHostgroupsParams extends ParsedArgs {
search_name: string | undefined
constructor(args?: any) {
super(args);
if ("search_name" in args && typeof (args.search_name) == "string") {
this.search_name = args.search_name
delete args.search_name
}
}
}
export type ZabbixQueryHostgroupsResult = {
groupid: string,
name: string,
uuid: string
}
export class ZabbixQueryHostgroupsRequest extends ZabbixRequest<ZabbixQueryHostgroupsResult[],
ZabbixQueryHostgroupsParams> {
constructor(authToken?: string | null, cookie?: string | null, hostGroupReadPermissions?: any) {
super("hostgroup.get", authToken, cookie, hostGroupReadPermissions,);
}
createZabbixParams(args?: ZabbixQueryHostgroupsParams): ZabbixParams {
let search = args?.search_name ? {
"search": {
"name": [
args.search_name
]
}
} : undefined
return {
"searchWildcardsEnabled": true,
"output": ["groupid", "name", "uuid"],
...args?.zabbix_params,
...search
}
};
}
export class GroupHelper {
public static groupFullName(groupName: string) {
return groupName == ZABBIX_EDGE_DEVICE_BASE_GROUP ? groupName : `${ZABBIX_EDGE_DEVICE_BASE_GROUP}/${GroupHelper.groupShortName(groupName)}`
}
static groupShortName(groupName: string) {
return groupName.replace(FIND_ZABBIX_EDGE_DEVICE_BASE_GROUP_PREFIX, "")
}
public static async findHostGroupIdsByName(groupNames: string[], zabbixApi: ZabbixAPI, zabbixAuthToken?: string, cookie?: string) {
let result: number[] = []
for (let groupName of groupNames) {
let queryGroupsArgs = new ZabbixQueryHostgroupsParams({
filter_name: GroupHelper.groupFullName(groupName)
});
let groups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, queryGroupsArgs)
if (isZabbixErrorResult(groups) || !groups?.length) {
logger.error(`Unable to find groupName=${groupName}`)
return []
}
result.push(...groups.map((group) => Number(group.groupid)))
}
return result
}
}

View file

@ -0,0 +1,303 @@
import {Host, ZabbixHost} from "../generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
import {
isZabbixErrorResult,
ParsedArgs,
ZabbixErrorResult,
ZabbixParams,
ZabbixRequest,
ZabbixResult
} from "./zabbix-request.js";
import {QueryZabbixItemResponse} from "./zabbix-items.js";
import {ZabbixExportValue, ZabbixHistoryGetParams, ZabbixQueryHistoryRequest} from "./zabbix-history.js";
export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult> extends ZabbixRequest<T> {
public static PATH = "host.get";
constructor(path: string, authToken?: string | null, cookie?: string | null) {
super(path, authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
selectParentTemplates: [
"templateid",
"name"
],
selectTags: [
"tag",
"value"
],
selectInheritedTags: [
"tag",
"value"
],
selectHostGroups: ["groupid", "name", "uuid"],
output: [
"hostid",
"host",
"name",
"hostgroups",
"description",
"parentTemplates"
]
};
}
}
export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest<Host[]> {
public static PATH = "host.get.meta";
constructor(authToken?: string | null, cookie?: string | null) {
super(ZabbixQueryHostsMetaRequest.PATH, authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
inheritedTags: true
};
}
}
export class ZabbixQueryHostsWithDeviceTypeMetaRequest extends ZabbixQueryHostsGenericRequest<Host[]> {
public static PATH = "host.get.meta_with_device_type"
constructor(authToken?: string | null, cookie?: string | null) {
super(ZabbixQueryHostsWithDeviceTypeMetaRequest.PATH, authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
tags: [
{
"tag": "deviceType",
"operator": 4
}
],
inheritedTags: true
};
}
}
export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult> extends ZabbixQueryHostsGenericRequest<T> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
selectItems: [
"itemid",
"key_",
"lastvalue",
"lastclock",
"name",
"type",
"value_type",
"status",
],
output: [
"hostid",
"host",
"name",
"hostgroup",
"items",
"description",
"parentTemplates"
],
};
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<ZabbixErrorResult | T> {
let result = await super.executeRequestReturnError(zabbixAPI, args);
if (result && !isZabbixErrorResult(result)) {
for (let device of <ZabbixHost[]>result) {
for (let item of device.items || []) {
if (!item.lastclock ) {
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
if (isZabbixErrorResult(values)) {
return values;
}
if (values.length) {
let latestValue = values[0];
item.lastvalue = latestValue.value;
item.lastclock = latestValue.clock;
} else {
item.lastvalue = null;
item.lastclock = null;
}
}
}
}
}
return result;
}
}
export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult> extends ZabbixQueryHostsGenericRequestWithItems<T> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
selectInventory: [
"location", "location_lat", "location_lon"
]
};
}
}
export class ZabbixQueryHostsRequestWithItemsAndInventory extends ZabbixQueryHostsGenericRequestWithItemsAndInventory<ZabbixHost[]> {
constructor(authToken?: string | null, cookie?: string) {
super("host.get.with_items", authToken, cookie);
}
}
export class ZabbixQueryHostWithInventoryRequest extends ZabbixRequest<any> {
constructor(authToken?: string | null, cookie?: string) {
super("host.get.with_inventory", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
selectInventory: [
"location", "location_lat", "location_lon"
],
output: [
"hostid",
"host",
"name",
"hostgroup",
"description",
"parentTemplates"
],
};
}
}
const isZabbixCreateHostInputParams = (value: ZabbixParams): value is ZabbixCreateHostInputParams => "host" in value && !!value.host;
export interface ZabbixCreateHostInputParams extends ZabbixParams {
host: string
name: string
description: string
location?: {
name: String,
location_lat?: String,
location_lon?: String,
}
templateids?: [number];
hostgroupids?: [number];
additionalParams?: [number];
}
class ZabbixCreateHostParams implements ZabbixParams {
constructor(inputParams: ZabbixCreateHostInputParams) {
this.host = inputParams.host;
this.name = inputParams.name;
this.description = inputParams.description;
if (inputParams.location) {
this.inventory = {
location: inputParams.location.name,
location_lat: inputParams.location.location_lat,
location_lon: inputParams.location.location_lon,
}
}
if (inputParams.templateids) {
this.templates = inputParams.templateids.map((templateid) => {
return {templateid: templateid}
});
}
if (inputParams.hostgroupids) {
this.groups = inputParams.hostgroupids.map((groupid) => {
return {groupid: groupid}
});
}
}
host: string
name: string
description: string
inventory?: {
location: String
location_lat?: String
location_lon?: String
}
templates?: any
groups?: any
}
export class ZabbixCreateHostRequest extends ZabbixRequest<{
hostids: number[]
}> {
constructor(authToken?: string | null, cookie?: string) {
super("host.create", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
if (args && isZabbixCreateHostInputParams(args.zabbix_params)) {
return {...new ZabbixCreateHostParams(args.zabbix_params), ...args.zabbix_params.additionalParams};
}
return args?.zabbix_params || {};
}
}
export class ZabbixQueryHostRequest extends ZabbixQueryHostsGenericRequest<any> {
constructor(authToken?: string | null, cookie?: string | null) {
super("host.get", authToken, cookie);
}
}
export class ZabbixCreateOrFindHostRequest extends ZabbixCreateHostRequest {
constructor(protected groupid: number, protected templateid: number, authToken?: string | null, cookie?: string,) {
super(authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return super.createZabbixParams(args);
}
async prepare(zabbixAPI: ZabbixAPI, args?: ParsedArgs) {
// Lookup host of appropriate type (by template) and groupName
// or create one if not found. If multiple hosts are found the first
// will be taken
let queryHostArgs = new ParsedArgs({
groupids: this.groupid,
templateids: this.templateid,
});
let hosts: {
hostid: number
}[] = await new ZabbixQueryHostRequest(this.authToken, this.cookie)
.executeRequestThrowError(zabbixAPI, queryHostArgs)
// logger.debug("Query hosts args=", JSON.stringify(queryHostArgs), "lead to result=", JSON.stringify(hosts));
if (hosts && hosts.length > 0) {
// If we found a host and return it as prep result the execution of the create host request will be skipped
this.prepResult = {
hostids: [hosts[0].hostid]
}
}
return this.prepResult;
}
}

View file

@ -0,0 +1,192 @@
import {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult, ZabbixValueType} from "./zabbix-request.js";
export class ZabbixQueryItemsMetaRequest extends ZabbixRequest<any> {
createZabbixParams(args?: ParsedArgs) {
return {
"templated": false,
output: [
"itemid",
"key_",
"hostid"
], ...args?.zabbix_params
};
}
}
export type QueryZabbixItemResponse = {
value_type: string;
itemid: string,
name: string,
status?: string,
key_?: string,
lastvalue: string | null
lastclock: string | null
tags?: {
tag: string,
value: string
}[]
hosts?: {
hostid: number,
host: string,
templateid?: number,
name: string
}[]
}
export class ZabbixQueryItemsRequest extends ZabbixRequest<QueryZabbixItemResponse[]> {
constructor(authToken?: string | null, cookie?: string) {
super("item.get", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
"templated": false,
"selectHosts": [
"templateid",
"hostid",
"host",
"name",
"description",
],
"selectTags": [
"tag",
"value"
],
output: [
"itemid",
"name",
"key_",
"hostid",
"status",
"type",
"description",
], ...args?.zabbix_params
};
}
}
export class ZabbixQueryItemsByIdRequest extends ZabbixRequest<QueryZabbixItemResponse[]> {
constructor(authToken?: string | null, cookie?: string) {
super("item.get.itembyid", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
let filter: { key_: string | null } | null = null
if (args?.zabbix_params?.hasOwnProperty("id")) {
// @ts-ignore
args.zabbix_params["filter"] = {
// @ts-ignore
...args?.zabbix_params.filter, "key_": args?.zabbix_params.id
}
// @ts-ignore
delete args.zabbix_params["id"]
}
return {
filter: filter,
"selectTags": ["tag", "value"],
"inheritedTags": true,
"output": [
"lastvalue",
"lastclock",
"value_type",
"hostid",
"itemid",
"name",
"status",
"key_"
], ...args?.zabbix_params,
}
};
}
export interface ZabbixStoreValueInItemParams extends ZabbixParams {
hostid?: number
itemid?: number
key: string
name: string
tags: {
tag: string,
value?: string
}[]
value: Object
}
const isStoreValueInItem = (value: ZabbixParams): value is ZabbixStoreValueInItemParams =>
"hostid" in value && !!value.hostid && "name" in value && "key" in value && "value" in value;
const isUpdateValueInItemParams = (value: ZabbixParams): value is ZabbixUpdateValueInItemParams =>
"itemid" in value && !!value.itemid && isStoreValueInItem(value);
export interface ZabbixUpdateValueInItemParams extends ZabbixStoreValueInItemParams {
itemid: number
}
export enum ZabbixItemType {
ZABBIX_TRAPPER = 2,
ZABBIX_SCRIPT = 21
}
export class ZabbixCreateOrUpdateStorageItemRequest extends ZabbixRequest<any> {
static MAX_ZABBIX_ITEM_STORAGE_PERIOD = "9125d"; // Maximum possible value is 25 years, which corresponds to 9125 days
createZabbixParams(args?: ParsedArgs): ZabbixParams {
if (args && isStoreValueInItem(args?.zabbix_params)) {
// Attention!! Zabbix status
// can not be used as expected:
// 1. Status 0 means enabled, all other values mean disabled
// 2. If the status of the item is disabled the value will not be
// evaluated - this means we can't use the item status to reflect
// the activation status of the controlProgram, as we also want
// to read the values of disabled controlPrograms..
let createOrUpdateItemParams = {
key_: args.zabbix_params.key,
name: args.zabbix_params.name,
tags: args.zabbix_params.tags,
"type": ZabbixItemType.ZABBIX_TRAPPER.valueOf(),
"history": ZabbixCreateOrUpdateStorageItemRequest.MAX_ZABBIX_ITEM_STORAGE_PERIOD,
"value_type": ZabbixValueType.TEXT.valueOf()
}
if (isUpdateValueInItemParams(args.zabbix_params)) {
return {
itemid: args.zabbix_params.itemid,
...createOrUpdateItemParams
};
}
return {
hostid: args.zabbix_params.hostid,
...createOrUpdateItemParams
}
}
return args?.zabbix_params || {};
}
}
export interface ZabbixDeleteItemResponse extends ZabbixResult {
itemids: {
itemid: string | string[]
}
}
export class ZabbixDeleteItemRequest extends ZabbixRequest<ZabbixDeleteItemResponse> {
constructor(authToken?: string | null, cookie?: string) {
super("item.delete", authToken, cookie);
}
}
export interface ZabbixCreateOrUpdateItemResponse extends ZabbixResult {
"itemids": string[]
}
export class ZabbixCreateOrUpdateItemRequest extends ZabbixRequest<ZabbixCreateOrUpdateItemResponse> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
}

View file

@ -0,0 +1,16 @@
import {ParsedArgs, ZabbixParams, ZabbixRequest} from "./zabbix-request.js";
import {UserRoleModule} from "../generated/graphql.js";
export class ZabbixQueryModulesRequest extends ZabbixRequest<UserRoleModule[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("module.get", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend"
}
}
}

View file

@ -0,0 +1,66 @@
import {ParsedArgs, ZabbixRequest} from "./zabbix-request.js";
class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest<
{
groupid: string,
name: string
}[]> {
constructor(authToken?: string | null, cookie?: string) {
super("templategroup.get.permissions", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
"output": [
"usrgrpid",
"name",
"gui_access",
"users_status"
], ...args?.zabbix_params,
"selectTemplateGroupRights": [
"id",
"permission"
]
};
}
}
interface ZabbixUserGroupResponse {
usrgrpid: string,
name: string,
gui_access: string,
users_status: string,
templategroup_rights:
{
id: string,
permission: string
}[]
}
class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGroupResponse[]> {
constructor(authToken?: string | null, cookie?: string) {
super("usergroup.get.permissions", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
...super.createZabbixParams(args),
"params": {
"output": [
"groupid",
"name"
],
"searchWildcardsEnabled": true,
"search": {
"name": [
"Permissions/*"
]
}
},
"id": 1
};
}
}

View file

@ -0,0 +1,530 @@
import {ApiError, InputMaybe, QueryHasPermissionsArgs, UserPermission} from "../generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
import {ApiErrorCode, Permission, PermissionNumber} from "../model/model_enum_values.js";
import {logger} from "../logging/logger.js";
import {GraphQLError} from "graphql";
class ZabbixRequestBody {
public jsonrpc = "2.0"
public method
public id = 1
public params?: ZabbixParams
constructor(method: string) {
this.method = method;
}
}
export interface ZabbixResult {
}
export type ZabbixErrorResult = {
error: ApiError
}
export const isZabbixErrorResult = (value: any): value is ZabbixErrorResult => value instanceof Object && "error" in value && !!value.error;
export interface ZabbixParams {
}
export interface ZabbixWithTagsParams extends ZabbixParams {
tags?: { tag: string; operator: number; value: any; }[]
}
export enum ZabbixValueType {
TEXT = 4,
}
export class ParsedArgs {
public name_pattern?: string
public distinct_by_name?: boolean;
public zabbix_params: ZabbixParams[] | ZabbixParams
constructor(params?: any) {
if (Array.isArray(params)) {
this.zabbix_params = params.map(arg => this.parseArgObject(arg))
} else if (params instanceof Object) {
this.zabbix_params = this.parseArgObject(params)
}
}
getParam(paramName: string): any {
if (this.zabbix_params instanceof Array) {
return undefined
}
// @ts-ignore
return paramName in this.zabbix_params ? this.zabbix_params[paramName] : undefined
}
parseArgObject(args?: Object) {
let result: ZabbixParams
if (args) {
if ("name_pattern" in args && typeof args["name_pattern"] == "string") {
if (args["name_pattern"]) {
this.name_pattern = args["name_pattern"]
}
delete args["name_pattern"]
}
if ("distinct_by_name" in args) {
this.distinct_by_name = !(!args["distinct_by_name"])
delete args["distinct_by_name"]
}
if ("groupidsbase" in args) {
if (!("groupids" in args) || !args.groupids) {
// @ts-ignore
args["groupids"] = args.groupidsbase
}
delete args.groupidsbase
}
let filterTagStatements: { tag: string; operator: number; value: any; }[] = []
let filterStatements = {}
for (let argsKey in args) {
// @ts-ignore
let argsValue = args[argsKey]
if (argsKey.startsWith("tag_") && argsValue !== null) {
let argsArray = Array.isArray(argsValue) ? argsValue : [argsValue]
argsArray.forEach((tagValue) => {
filterTagStatements.push({
"tag": argsKey.slice(4),
"operator": 1,
"value": tagValue,
})
})
// @ts-ignore
delete args[argsKey]
}
if (argsKey.startsWith("filter_") && argsValue) {
// @ts-ignore
filterStatements[argsKey.slice(7)] = argsValue
// @ts-ignore
delete args[argsKey]
}
}
if (Object.keys(filterStatements).length) {
args = {
...args,
filter: filterStatements
}
}
if (filterTagStatements?.length) {
let tagsFilter: ZabbixWithTagsParams = {
tags: filterTagStatements
}
result = {
...tagsFilter,
...args,
inheritedTags: true,
}
} else {
result = args
}
} else {
result = {}
}
if (this.name_pattern) {
if ("search" in result) {
(<any> result.search).name = this.name_pattern
} else {
(<any> result).search = {
name: this.name_pattern,
}
}
}
return result
}
}
export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> {
protected requestBodyTemplate: ZabbixRequestBody;
protected method: string
protected prepResult: T | ZabbixErrorResult | undefined = undefined
constructor(public path: string, public authToken?: string | null, public cookie?: string | null,
protected permissionsNeeded?: QueryHasPermissionsArgs) {
this.method = path.split(".", 2).join(".");
this.requestBodyTemplate = new ZabbixRequestBody(this.method);
}
createZabbixParams(args?: A): ZabbixParams {
return args?.zabbix_params || {}
}
getRequestBody(args?: A, zabbixParams?: ZabbixParams): ZabbixRequestBody {
let params: ZabbixParams
if (Array.isArray(args?.zabbix_params)) {
params = args?.zabbix_params.map(paramsObj => {
return {...this.requestBodyTemplate.params, ...paramsObj}
})
} else {
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)}
}
return params ? {
...this.requestBodyTemplate,
params: params
} : this.requestBodyTemplate
};
headers() {
let headers: {
"Content-Type": string
Accept: string
'Access-Control-Allow-Headers': string
Cookie?: string,
Authorization?: string
} = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Access-Control-Allow-Headers': 'Content-Type'
};
if (this.cookie) {
headers.Cookie = this.cookie
}
if (this.authToken) {
headers.Authorization = `Bearer ${this.authToken}`
}
return headers
}
async assureUserPermissions(zabbixAPI: ZabbixAPI) {
if (this.permissionsNeeded &&
!await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) {
return {
error: {
message: "User does not have the required permissions",
code: ApiErrorCode.PERMISSION_ERROR,
path: this.path,
args: this.permissionsNeeded
}
}
} else {
return undefined
}
}
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult | undefined> {
// If prepare returns something else than undefined the execution will be skipped and the
// result returned
this.prepResult = await this.assureUserPermissions(zabbixAPI);
return this.prepResult;
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult> {
let prepareResult = await this.prepare(zabbixAPI, args);
if (prepareResult) {
return prepareResult;
}
let requestBody = this.getRequestBody(args);
try {
const result_promise = zabbixAPI.post<T | ZabbixErrorResult>(this.path, {
body: {...requestBody}, headers: this.headers()
});
return result_promise.then(response => {
if (isZabbixErrorResult(response)) {
return response as ZabbixErrorResult;
}
return response as T;
})
} catch (e) {
const msg = `Unable to execute zabbix request body=${JSON.stringify(requestBody)}: ${JSON.stringify(e)}`
logger.error(msg)
return {
error: {
code: -1,
message: msg,
data: e
}
} as ZabbixErrorResult
}
}
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A): Promise<T> {
let response = await this.executeRequestReturnError(zabbixApi, args);
if (isZabbixErrorResult(response)) {
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
extensions: {
path: response.error.path || this.path,
args: response.error.args || args,
code: response.error.code,
data: response.error.data
},
});
} else {
return response as unknown as T;
}
}
}
export class ZabbixCreateOrUpdateParams extends ParsedArgs {
constructor(args: any, public dryRun = true) {
super(args);
}
}
export class ZabbixCreateOrUpdateRequest<
T extends ZabbixResult,
P extends ZabbixRequest<ZabbixResult>,
A extends ZabbixCreateOrUpdateParams = ZabbixCreateOrUpdateParams> extends ZabbixRequest<T, A> {
constructor(public entity: string,
public updateExistingIdFieldname: string,
private prepareType: new (authToken?: string | null, cookie?: string | null) => P, authToken?: string | null, cookie?: string | null) {
super(entity + ".create.orupdate", authToken, cookie);
}
public message: string = "";
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T | undefined> {
let prepResult = await super.prepare(zabbixAPI, args);
let nameParam = args?.getParam("name");
if (prepResult || !nameParam) {
return prepResult;
}
let existingItems = await new this.prepareType(this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter: {
name: nameParam
}
})) as Record<string, any>[] | ZabbixErrorResult;
if (isZabbixErrorResult(existingItems)) {
this.message = "Error getting existing " + this.entity + "(s)";
this.prepResult = existingItems;
return existingItems;
}
if (existingItems.length > 1) {
this.message = "Multiple existing " + this.entity + "(s) found for existing args";
this.prepResult = {
error: {
code: ApiErrorCode.ZABBIX_MULTIPLE_USERGROUPS_FOUND,
message: this.message,
data: args,
}
}
return this.prepResult;
}
if (existingItems.length == 1) {
this.message = "Updating existing user group";
if (args?.dryRun) {
this.prepResult = {
error: {
code: ApiErrorCode.OK,
message: "Not updating existing user group, dry run enabled",
data: args,
}
}
} else {
let updateParams: Record<string, any> = {
...args?.zabbix_params
}
updateParams[this.updateExistingIdFieldname] = existingItems[0][this.updateExistingIdFieldname];
this.prepResult = await new ZabbixRequest<any>(this.entity + ".update", this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs(updateParams));
}
} else {
this.message = "Creating " + this.entity + " - name not found";
if (args?.dryRun) {
this.prepResult = {
error: {
code: ApiErrorCode.OK,
message: "Not creating " + this.entity + ", dry run enabled",
data: args,
}
}
}
}
return this.prepResult
}
}
class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest<
{
groupid: string,
name: string
}[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("templategroup.get.permissions", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
...super.createZabbixParams(args),
output: [
"groupid",
"name"
],
searchWildcardsEnabled: true,
search: {
name: [
ZabbixPermissionsHelper.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX + "/*"
]
}
};
}
}
interface ZabbixUserGroupResponse {
usrgrpid: string,
name: string,
gui_access: string,
users_status: string,
templategroup_rights:
{
id: string,
permission: Permission
}[]
}
class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGroupResponse[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("usergroup.get.permissions", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
...super.createZabbixParams(args),
"output": [
"usrgrpid",
"name",
"gui_access",
"users_status"
], ...args?.zabbix_params,
"selectTemplateGroupRights": [
"id",
"permission"
]
};
}
}
export class ZabbixPermissionsHelper {
private static permissionObjectNameCache: Map<string, string | null> = new Map()
public static ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX = process.env.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX || "Permissions"
public static async getUserPermissions(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string, cookie?: string,
objectNames?: InputMaybe<string[]> | undefined): Promise<UserPermission[]> {
return Array.from((await this.getUserPermissionNumbers(zabbixAPI, zabbixAuthToken, cookie, objectNames)).entries()).map(value => {
return {
objectName: value[0],
permission: this.mapPermissionToZabbixEnum(value[1])
}
});
}
public static async getUserPermissionNumbers(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string | null, cookie?: string | null, objectNamesFilter?: InputMaybe<string[]> | undefined): Promise<Map<string, PermissionNumber>> {
const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
// Prepare the list of templateIds that are not loaded yet
const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => usergroup.templategroup_rights.map(templateGroupRight => templateGroupRight.id)));
// Remove all templateIds that are already in the permissionObjectNameCache
templateIdsToLoad.forEach(id => {
if (this.permissionObjectNameCache.has(id)) {
templateIdsToLoad.delete(id);
}
})
if (templateIdsToLoad.size > 0) {
// Load all templateIds that are not in the permissionObjectNameCache
const missingPermissionGroupNames = await new ZabbixQueryTemplateGroupPermissionsRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ParsedArgs({groupids: Array.from(templateIdsToLoad)}));
missingPermissionGroupNames.forEach(group => {
this.permissionObjectNameCache.set(group.groupid, group.name.replace(ZabbixPermissionsHelper.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX + "/", ""))
})
}
// Merge the permissions from the user groups. The merge function will first merge the permissions from the template groups
let permissions = new Map<string, PermissionNumber>();
userGroupPermissions.forEach(usergroup => {
permissions = this.mergeTemplateGroupPermissions(usergroup, permissions, objectNamesFilter);
})
return permissions;
}
private static mergeTemplateGroupPermissions(usergroup: ZabbixUserGroupResponse,
currentTemplateGroupPermissions: Map<string, PermissionNumber>,
objectNames: InputMaybe<string[]> | undefined): Map<string, PermissionNumber> {
// First we have to find the minimum permission for each template group as this is always superseeding the higher permission if it is set within a user group
let minPermissionsInUserGroup: Map<string, PermissionNumber> = new Map();
let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames);
usergroup.templategroup_rights.forEach(templateGroupPermission => {
const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id);
if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) {
const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber;
const minPermissionWithinThisGroup = minPermissionsInUserGroup.get(objectName);
if (minPermissionWithinThisGroup == undefined || minPermissionWithinThisGroup > permissionValue) {
minPermissionsInUserGroup.set(objectName, permissionValue);
}
}
})
// Then we have to find the highest permission compared to the permissions resulting from other user groups as on a
// user group level the higher permission is always superseeding the lower permission
minPermissionsInUserGroup.forEach((minPermissionInUserGroup, objectName) => {
const maxPermissionBetweenGroups = currentTemplateGroupPermissions.get(objectName);
if (maxPermissionBetweenGroups == undefined || maxPermissionBetweenGroups < minPermissionInUserGroup) {
currentTemplateGroupPermissions.set(objectName, minPermissionInUserGroup);
}
})
return currentTemplateGroupPermissions;
}
private static mapZabbixPermission(zabbixPermission: Permission): PermissionNumber {
switch (zabbixPermission) {
case Permission.Read:
return PermissionNumber.Read
case Permission.ReadWrite:
return PermissionNumber.ReadWrite
case Permission.Deny:
default:
return PermissionNumber.Deny
}
}
private static mapPermissionToZabbixEnum(permission: PermissionNumber): Permission {
switch (permission) {
case PermissionNumber.Read:
return Permission.Read
case PermissionNumber.ReadWrite:
return Permission.ReadWrite
case PermissionNumber.Deny:
default:
return Permission.Deny
}
}
private static createMatcherFromWildcardArray(array: InputMaybe<string[]> | undefined): RegExp | undefined {
if (!array) {
return undefined;
}
// Escape all values in the array and create regexp that allows the * wildcard which will be a .* in the regexp
return new RegExp(array.map(value => "^" + this.escapeRegExp(value).replace(/\\\*/gi, ".*") + "$").join("|"));
}
private static escapeRegExp(value: string) {
let result = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
return result;
}
public static async hasUserPermissions(zabbixAPI: ZabbixAPI, args: QueryHasPermissionsArgs, zabbixAuthToken?: string | null, cookie?: string | null): Promise<boolean> {
let permissions = await this.getUserPermissionNumbers(zabbixAPI, zabbixAuthToken, cookie);
for (const permission of args.permissions) {
const existingPermission = permissions.get(permission.objectName);
if (permission.permission != Permission.Deny) {
if (existingPermission == undefined || existingPermission < this.mapZabbixPermission(permission.permission)) {
return false;
}
}
}
return true;
}
}

View file

@ -0,0 +1,89 @@
import {ZabbixAPI} from "./zabbix-api.js";
import {ApiErrorCode} from "../model/model_enum_values.js";
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult, ZabbixParams, ZabbixRequest} from "./zabbix-request.js";
import {ZabbixQueryHostsMetaRequest} from "./zabbix-hosts.js";
export class ZabbixForceCacheReloadParams extends ParsedArgs {
constructor(public hostid: number) {
super();
}
}
export class ZabbixForceCacheReloadRequest extends ZabbixRequest<{
response: string
value: string
}, ZabbixForceCacheReloadParams> {
private static ZABBIX_RELOAD_CACHE_SCRIPT_NAME = "Reload configuration cache";
private static reloadCacheScriptId = 0;
private reloadCacheHostId = 0;
private static ZABBIX_RELOAD_CACHE_SCRIPT_PARAMS = {
"name": this.ZABBIX_RELOAD_CACHE_SCRIPT_NAME,
"command": "/usr/lib/zabbix/alertscripts/config_cache_reload.sh",
"host_access": "2",
"description": "Reload the configuration cache making config changes like new hosts/items visible to zabbix_server. Without this new values cannot be received.",
"type": "0",
"execute_on": "1",
"timeout": "30s",
"scope": "2"
};
constructor(authToken?: string | null, cookie?: string | null) {
super("script.execute", authToken, cookie);
}
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixForceCacheReloadParams) {
this.reloadCacheHostId = args?.hostid || 0;
if (!this.reloadCacheHostId) {
let someHost = await new ZabbixQueryHostsMetaRequest(this.authToken, this.cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({limit: 1}))
if (!isZabbixErrorResult(someHost) && someHost && someHost.length && someHost[0].hostid) {
this.reloadCacheHostId = Number(someHost[0].hostid)
} else {
this.prepResult = {
error: {
message: "Unable to find host for executing scripts",
code: ApiErrorCode.ZABBIX_HOST_NOT_FOUND,
path: this.path,
args: args,
}
} as ZabbixErrorResult
return this.prepResult
}
}
if (!args?.zabbix_params?.hasOwnProperty("scriptid") && !ZabbixForceCacheReloadRequest.reloadCacheScriptId) {
let scriptResult = await new ZabbixRequest<{
scriptid: string
}[]>("script.get", this.authToken, this.cookie).executeRequestThrowError(zabbixAPI, new ParsedArgs(
{filter_name: ZabbixForceCacheReloadRequest.ZABBIX_RELOAD_CACHE_SCRIPT_NAME}))
if (scriptResult && scriptResult.length && scriptResult[0].scriptid) {
ZabbixForceCacheReloadRequest.reloadCacheScriptId = Number(scriptResult[0].scriptid)
} else {
let createScriptResult = await new ZabbixRequest<{
scriptids: string[]
}>("script.create", this.authToken, this.cookie).executeRequestThrowError(zabbixAPI,
new ParsedArgs(ZabbixForceCacheReloadRequest.ZABBIX_RELOAD_CACHE_SCRIPT_PARAMS))
if (!isZabbixErrorResult(createScriptResult) && createScriptResult?.scriptids && createScriptResult.scriptids.length) {
ZabbixForceCacheReloadRequest.reloadCacheScriptId = Number(createScriptResult.scriptids[0]);
} else {
this.prepResult = {
error: {
message: "Unable to find or create script for reloading cache",
code: ApiErrorCode.ZABBIX_SCRIPT_NOT_FOUND,
data: createScriptResult,
path: this.path,
args: args,
}
} as ZabbixErrorResult
}
}
return this.prepResult
}
}
createZabbixParams(_args?: ZabbixForceCacheReloadParams): ZabbixParams {
return {
"scriptid": ZabbixForceCacheReloadRequest.reloadCacheScriptId,
"hostid": this.reloadCacheHostId,
}
}
}

View file

@ -0,0 +1,32 @@
import { ZabbixRequest } from "./zabbix-request.js";
export interface ZabbixQueryTemplateResponse {
templateid: string,
uuid: string,
name: string,
}
export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> {
constructor(authToken?: string | null, cookie?: string | null,) {
super("template.get", authToken, cookie);
}
}
export interface ZabbixQueryTemplateGroupResponse {
groupid: string,
name: string,
uuid: string
}
export class ZabbixQueryTemplateGroupRequest extends ZabbixRequest<ZabbixQueryTemplateGroupResponse[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("templategroup.get", authToken, cookie);
}
}

View file

@ -0,0 +1,330 @@
import {
isZabbixErrorResult,
ParsedArgs,
ZabbixCreateOrUpdateParams,
ZabbixCreateOrUpdateRequest,
ZabbixErrorResult,
ZabbixParams,
ZabbixRequest,
ZabbixResult
} from "./zabbix-request.js";
import {
ApiError,
ImportUserRightResult, Permission,
UserGroup,
UserGroupInput,
ZabbixGroupRight,
ZabbixGroupRightInput
} from "../generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js";
import {ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
import {ApiErrorCode} from "../model/model_enum_values.js";
abstract class ZabbixPrepareGetTemplatesAndHostgroupsRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> {
protected templategroups: ZabbixQueryTemplateGroupResponse[];
protected hostgroups: ZabbixQueryHostgroupsResult[];
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T | undefined> {
let prepResult = await super.prepare(zabbixAPI, args);
if (prepResult) {
return prepResult;
}
let templategroups = await new ZabbixQueryTemplateGroupRequest(this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI);
if (isZabbixErrorResult(templategroups)) {
this.prepResult = templategroups;
return templategroups;
}
this.templategroups = templategroups;
let hostgroups =
await new ZabbixQueryHostgroupsRequest(this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI);
if (isZabbixErrorResult(hostgroups)) {
this.prepResult = hostgroups;
return hostgroups;
}
this.hostgroups = hostgroups;
return undefined
}
}
export class ZabbixExportUserGroupArgs extends ParsedArgs {
public exclude_hostgroups_pattern?: RegExp | undefined = undefined;
constructor(name_pattern?: string | null, exclude_hostgroups_pattern_str?: string | null) {
super(name_pattern? {name_pattern: name_pattern} : undefined);
if (exclude_hostgroups_pattern_str) {
this.exclude_hostgroups_pattern = new RegExp(exclude_hostgroups_pattern_str);
}
}
}
export class ZabbixExportUserGroupsRequest extends ZabbixPrepareGetTemplatesAndHostgroupsRequest<
UserGroup[], ZabbixExportUserGroupArgs> {
constructor(authToken?: string | null, cookie?: string) {
super("usergroup.get.withuuids", authToken, cookie);
}
createZabbixParams(args?: ZabbixExportUserGroupArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend",
selectTemplateGroupRights: "extend",
selectHostGroupRights: "extend"
};
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixExportUserGroupArgs): Promise<ZabbixErrorResult | UserGroup[]> {
let result = await super.executeRequestReturnError(zabbixAPI, args);
if (!isZabbixErrorResult(result)) {
for (let userGroup of result) {
for (let template_permission of userGroup.templategroup_rights || []) {
for (let templategroup of this.templategroups) {
if (templategroup.groupid == template_permission.id.toString()) {
template_permission.uuid = templategroup.uuid;
template_permission.name = templategroup.name;
break;
}
}
}
let filtered_hostgroup_permission: ZabbixGroupRight [] = [];
for (let hostgroup_permission of userGroup.hostgroup_rights || []) {
for (let hostgroup of this.hostgroups) {
if (hostgroup.groupid == hostgroup_permission.id.toString()) {
hostgroup_permission.uuid = hostgroup.uuid;
hostgroup_permission.name = hostgroup.name;
break;
}
}
if (!args?.exclude_hostgroups_pattern || !hostgroup_permission.name || !args.exclude_hostgroups_pattern.test(hostgroup_permission.name)) {
filtered_hostgroup_permission.push(hostgroup_permission);
}
}
userGroup.hostgroup_rights = filtered_hostgroup_permission;
}
}
return result;
}
}
export class ZabbixQueryUserGroupsRequest extends ZabbixRequest<UserGroup[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("usergroup.get", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend",
};
}
}
export class ZabbixImportUserGroupsParams extends ParsedArgs {
constructor(public usergroups: UserGroupInput[], public dryRun = true) {
super();
}
}
export class ZabbixImportUserGroupsRequest
extends ZabbixPrepareGetTemplatesAndHostgroupsRequest<ImportUserRightResult[],
ZabbixImportUserGroupsParams> {
constructor(zabbixAuthToken: any, cookie: any) {
super("usergroup.create.import", zabbixAuthToken, cookie);
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixImportUserGroupsParams): Promise<ZabbixErrorResult | ImportUserRightResult[]> {
let prepareResult = await this.prepare(zabbixAPI, args);
if (prepareResult) {
return prepareResult;
}
let results: ImportUserRightResult[] = [];
let hostGroupsToPropagete: number[] = []
let createGroupRequest = new ZabbixCreateOrUpdateRequest<
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
for (let userGroup of args?.usergroups || []) {
let templategroup_rights = this.calc_templategroup_rights(userGroup);
let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
let errors: ApiError[] = [];
let params = new ZabbixCreateOrUpdateParams({
name: userGroup.name,
gui_access: userGroup.gui_access,
users_status: userGroup.users_status,
hostgroup_rights: hostgroup_rights.hostgroup_rights,
templategroup_rights: templategroup_rights.templategroup_rights,
}, args?.dryRun)
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
if (isZabbixErrorResult(result)) {
errors.push(result.error);
results.push(
{
name: userGroup.name,
errors: errors,
message: result.error.message || "Error creating user group",
}
)
} else {
hostGroupsToPropagete.push(
...hostgroup_rights.hostgroup_rights.map(
value => value.id));
results.push(
{
name: userGroup.name,
id: result.usrgrpids[0],
message: createGroupRequest.message,
errors: errors,
}
)
}
errors.push(...templategroup_rights.errors);
errors.push(...hostgroup_rights.errors);
}
// If user groups were imported: Propagate group permissions to group children
if (hostGroupsToPropagete.length > 0) {
// Propagate group permissions to group children, filter duplicate groupids first
await new ZabbixPropagateHostGroupsRequest(this.authToken, this.cookie)
.executeRequestThrowError(zabbixAPI,
new ZabbixPropagateHostGroupsParams(hostGroupsToPropagete))
}
return results;
}
calc_hostgroup_rights(usergroup: UserGroupInput): {
errors: ApiError[],
hostgroup_rights: ZabbixGroupRight[]
} {
let result: ZabbixGroupRight [] = [];
let errors: ApiError[] = [];
for (let hostgroup_right of usergroup.hostgroup_rights || []) {
let success = false;
let matchedName = "";
for (let hostgroup of this.hostgroups) {
if (hostgroup.uuid == hostgroup_right.uuid) {
result.push(
{
id: Number(hostgroup.groupid),
permission: hostgroup_right.permission,
}
)
success = true;
matchedName = hostgroup.name;
break;
}
}
if (success && hostgroup_right.name && hostgroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match`,
data: hostgroup_right,
}
)
}
if (!success) {
errors.push(
{
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`,
data: hostgroup_right,
}
)
}
}
return {
hostgroup_rights: result,
errors: errors,
};
}
calc_templategroup_rights(usergroup: UserGroupInput): {
errors: ApiError[],
templategroup_rights: ZabbixGroupRightInput[]
} {
let result: ZabbixGroupRight [] = [];
let errors: ApiError[] = [];
for (let templategroup_right of usergroup.templategroup_rights || []) {
let success = false;
let matchedName = "";
for (let templategroup of this.templategroups) {
if (templategroup.uuid == templategroup_right.uuid) {
result.push(
{
id: Number(templategroup.groupid),
permission: templategroup_right.permission,
}
)
success = true;
matchedName = templategroup.name
break;
}
}
if (success && templategroup_right.name && templategroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match`,
data: templategroup_right,
}
)
}
if (!success) {
errors.push(
{
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
message: `Templategroup with UUID ${templategroup_right.uuid} not found`,
data: templategroup_right,
}
)
}
}
return {
templategroup_rights: result,
errors: errors,
};
}
}
export type ZabbixCreateUserGroupResponse = {
usrgrpids: string[];
}
class ZabbixPropagateHostGroupsParams extends ParsedArgs {
constructor(public groups: number[]) {
super();
}
}
export class ZabbixPropagateHostGroupsRequest extends ZabbixRequest<ZabbixCreateUserGroupResponse,
ZabbixPropagateHostGroupsParams> {
constructor(authToken?: string | null, cookie?: string | null) {
super("hostgroup.propagate", authToken, cookie);
}
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
return {
groups: [...new Set(args?.groups || [])].map(value => {
return {
groupid: value
}
}) || [],
permissions: true
}
}
}

View file

@ -0,0 +1,175 @@
import {
isZabbixErrorResult,
ParsedArgs,
ZabbixCreateOrUpdateParams,
ZabbixCreateOrUpdateRequest,
ZabbixErrorResult,
ZabbixParams,
ZabbixRequest,
ZabbixResult
} from "./zabbix-request.js";
import {ApiError, ImportUserRightResult, UserRole, UserRoleInput, UserRoleModule} from "../generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
import {ZabbixQueryModulesRequest} from "./zabbix-module.js";
import {ApiErrorCode} from "../model/model_enum_values.js";
export class ZabbixPrepareGetModulesRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> {
protected modules: UserRoleModule[];
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T | undefined> {
let result = super.prepare(zabbixAPI, args);
if (!isZabbixErrorResult(result)) {
this.modules = await new ZabbixQueryModulesRequest(
this.authToken, this.cookie).executeRequestThrowError(zabbixAPI);
}
return result;
}
}
export class ZabbixQueryUserRolesRequest extends ZabbixPrepareGetModulesRequest<UserRole[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("role.get", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend",
selectRules: "extend",
};
}
async executeRequestReturnError(zabbixApi: ZabbixAPI, args?: ParsedArgs): Promise<UserRole[] | ZabbixErrorResult> {
let prepResult = await this.prepare(zabbixApi, args);
if (prepResult) {
return prepResult
}
let result = await super.executeRequestReturnError(zabbixApi, args);
if (isZabbixErrorResult(result)) {
return result
}
for (let userRole of result) {
for (let userRoleModule of userRole.rules?.modules || []) {
for (let module of this.modules) {
if (module.moduleid == userRoleModule.moduleid) {
userRoleModule.id = module.id;
userRoleModule.relative_path = module.relative_path;
break;
}
}
}
}
return result;
}
}
export class ZabbixImportUserRolesParams extends ParsedArgs {
constructor(public userRoles: UserRoleInput[], public dryRun: boolean = false) {
super();
}
}
export class ZabbixImportUserRolesRequest extends ZabbixPrepareGetModulesRequest<ImportUserRightResult[],
ZabbixImportUserRolesParams> {
constructor(zabbixAuthToken: any, cookie: any) {
super("role.create.import", zabbixAuthToken, cookie);
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixImportUserRolesParams): Promise<ImportUserRightResult[] | ZabbixErrorResult> {
let prepResult = await this.prepare(zabbixAPI, args);
if (isZabbixErrorResult(prepResult)) {
return prepResult
}
let results: ImportUserRightResult[] = [];
for (let userRole of args?.userRoles || []) {
let errors: ApiError[] = [];
let rules: any = undefined;
if (userRole.rules) {
let modules: UserRoleModule[] = []
for (let userRoleModule of userRole.rules.modules || []) {
let found = false;
for (let module of this.modules) {
if (module.moduleid == userRoleModule.moduleid &&
(!userRoleModule.id || module.id == userRoleModule.id)) {
found = true;
modules.push(
{
moduleid: userRoleModule.moduleid,
status: userRoleModule.status,
}
);
break;
}
if (module.id == userRoleModule.id) {
if (userRoleModule.moduleid && userRoleModule.moduleid != module.moduleid) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Module found and permissions set, but target moduleid=${module.moduleid} does not match`,
data: userRoleModule,
}
)
modules.push(
{
moduleid: module.moduleid,
status: userRoleModule.status,
}
)
found = true;
break;
}
}
}
if (!found) {
errors.push(
{
code: ApiErrorCode.ZABBIX_MODULE_NOT_FOUND,
message: `Module with ID ${userRoleModule.id} not found`,
data: userRoleModule,
}
)
}
}
rules = {
ui: userRole.rules.ui,
"ui.default_access": userRole.rules.ui_default_access,
modules: modules,
"modules.default_access": userRole.rules.modules_default_access,
"api.access": userRole.rules.api_access,
"api.mode": userRole.rules.api_mode,
api: userRole.rules.api,
actions: userRole.rules.actions,
"actions.default_access": userRole.rules.actions_default_access,
}
}
let params = new ZabbixCreateOrUpdateParams({
name: userRole.name,
type: userRole.type,
rules: rules,
}, args?.dryRun)
let createUserRoleRequest = new ZabbixCreateOrUpdateRequest<
{ roleids: string[] }, ZabbixQueryUserRolesRequest, ZabbixCreateOrUpdateParams>(
"role", "roleid", ZabbixQueryUserRolesRequest, this.authToken, this.cookie);
let result = await createUserRoleRequest.executeRequestReturnError(zabbixAPI, params);
if (isZabbixErrorResult(result)) {
errors.push(result.error);
results.push(
{
name: userRole.name,
errors: errors,
message: result.error.message || "Error creating user role",
}
)
} else {
results.push({
name: userRole.name,
id: result.roleids[0],
errors: errors,
message: createUserRoleRequest.message,
});
}
}
return results;
}
}

View file

@ -0,0 +1,118 @@
import {
ApiError,
DeviceValueExportResponse,
QueryExportDeviceValueHistoryArgs,
StorageItemType
} from "../generated/graphql.js";
import {ApiErrorCode, ApiErrorMessage} from "../model/model_enum_values.js";
import {QueryZabbixItemResponse, ZabbixQueryItemsRequest} from "../datasources/zabbix-items.js";
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js";
import {ZabbixHistoryGetParams, ZabbixQueryHistoryRequest} from "../datasources/zabbix-history.js";
import {zabbixAPI} from "../datasources/zabbix-api";
type FilterCombo = {
deviceKey: string,
attributeName: string
}
type ItemMapResponse = {
items?: Map<number, FilterCombo>,
error?: ApiError
}
export class HostValueExporter {
static async exportDeviceData(args: QueryExportDeviceValueHistoryArgs, zabbixAuthToken?: string, cookie?: string): Promise<DeviceValueExportResponse> {
let itemMapResponse: ItemMapResponse = await HostValueExporter.queryItemsForFilterArgs(args, zabbixAuthToken, cookie);
if (itemMapResponse.error || !itemMapResponse.items) {
return {
error: itemMapResponse.error
}
}
let itemMap = itemMapResponse.items;
let items = Array.from(itemMap.keys());
if (!items.length) {
return {
result: []
}
}
let history = await new ZabbixQueryHistoryRequest(zabbixAuthToken, cookie).executeRequestThrowError(
zabbixAPI, new ZabbixHistoryGetParams(
items, ["value", "itemid", "clock", "ns"],
args.limit,
args.type || StorageItemType.Float,
args.time_from, args.time_until,
["clock"],
args.sortOrder,
))
if (isZabbixErrorResult(history)) {
return {
error: {
data: history.error.data,
code: ApiErrorCode.ZABBIX_ITEM_NOT_FOUND,
message: ApiErrorMessage.ZABBIX_UNABLE_TO_RETRIEVE_HISTORY
}
}
}
return {
result: history.map(historyItem => {
let itemid = +historyItem.itemid!;
let filter = itemMap.get(itemid);
if (!filter) {
return undefined
}
let timestamp: Date = new Date((+historyItem.clock * 1000) + (historyItem.ns / 1000));
return {
attributeKey: filter?.attributeName,
value: historyItem.value,
deviceKey: filter?.deviceKey,
itemid: itemid,
timestamp: timestamp,
}
}).filter(result => !!result)
}
}
static async queryItemsForFilterArgs(args: QueryExportDeviceValueHistoryArgs, zabbixAuthToken?: string, cookie?: string): Promise<ItemMapResponse> {
let deviceKeys = args.deviceKey_filter
let attributeNames = args.attribute_filter
let items: QueryZabbixItemResponse[] | ZabbixErrorResult = await new ZabbixQueryItemsRequest(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs(
{
filter: {
host: deviceKeys,
key_: attributeNames
},
tags: [{"tag": "hasValue", "operator": 1, "value": "true"}]
}))
if (isZabbixErrorResult(items)) {
return {
error: {
data: items.error.data,
code: ApiErrorCode.ZABBIX_ITEM_NOT_FOUND,
message: ApiErrorMessage.ZABBIX_UNABLE_TO_RETRIEVE_ITEMS_ACCORDING_TO_FILTER
}
}
}
let result: Map<number, FilterCombo> = new Map();
items.forEach(item => {
let deviceKey = item.hosts?.length ? item.hosts[0].host : undefined
if (!item.itemid || !deviceKey || !item.key_) {
return
}
result.set(+item.itemid, {
deviceKey: deviceKey,
attributeName: item.key_
})
})
return {
items: result
}
}
}

View file

@ -0,0 +1,158 @@
import {
CreateHost,
CreateHostResponse,
CreateHostGroupResponse,
InputMaybe,CreateHostGroup
} from "../generated/graphql.js";
import {logger} from "../logging/logger.js";
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult} from "../datasources/zabbix-request.js";
import {CreateHostGroupResult, GroupHelper, ZabbixCreateHostGroupRequest} from "../datasources/zabbix-hostgroups.js";
import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api";
export class HostImporter {
public static getHostGroupHierarchyNames(hostGroups: Array<CreateHostGroup>) {
let resultSet: Set<CreateHostGroup> = new Set<CreateHostGroup>(hostGroups)
for (let group of hostGroups || []) {
let levelNames = group.groupName.split("/", hostGroups?.length - 1)
let leafName = ""
for (let level of levelNames) {
leafName += (leafName ? "/" + level : level)
resultSet.add({groupName: leafName})
}
}
return resultSet
}
public static async importHostGroups(hostGroups: InputMaybe<Array<CreateHostGroup>> | undefined, zabbixAuthToken?: string, cookie?: string) {
if (!hostGroups) {
return null
}
let result: CreateHostGroupResponse[] = []
for (let group of HostImporter.getHostGroupHierarchyNames(hostGroups)) {
let createGroupResult: CreateHostGroupResult | ZabbixErrorResult | undefined = undefined;
let groups = await GroupHelper.findHostGroupIdsByName([group.groupName], zabbixAPI, zabbixAuthToken, cookie)
let groupid = 0
let message: string | undefined = undefined
if (groups?.length) {
groupid = groups[0]
message = `Group ${group.groupName} already exists with groupid=${groupid} - skipping`
logger.debug(message)
} else {
createGroupResult = await new ZabbixCreateHostGroupRequest(zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI,
new ParsedArgs({
name: GroupHelper.groupFullName(group.groupName),
uuid: group.uuid
}))
if (isZabbixErrorResult(createGroupResult)) {
result.push(
{
groupName: group.groupName,
message: `Unable to create groupName=${group.groupName}: ${JSON.stringify(createGroupResult)}`,
error: createGroupResult!.error
}
)
continue
} else {
if (createGroupResult?.groupids?.length) {
groupid = Number(createGroupResult.groupids[0])
}
}
}
if (groupid) {
result.push(
{
groupName: group.groupName,
groupid: groupid,
message: message
}
)
} else {
result.push(
{
groupName: group.groupName,
message: `Unable to create groupName=${group.groupName}: ${JSON.stringify(createGroupResult)}`,
error: {
message: "Unknown error - no groupid returned",
}
}
)
}
}
return result
}
static async importHosts(devices: InputMaybe<Array<CreateHost>> | undefined, zabbixAuthToken?: string, cookie?: string) {
if (!devices) {
return null
}
let result: CreateHostResponse[] = []
for (let device of devices) {
let groupids = device.groupids
if (!groupids) {
groupids = await GroupHelper.findHostGroupIdsByName([ZABBIX_EDGE_DEVICE_BASE_GROUP, ...device.groupNames], zabbixAPI, zabbixAuthToken, cookie)
if (!groupids?.length) {
result.push(
{
deviceKey: device.deviceKey,
message: `Unable to find groupNames=${device.groupNames}`
}
)
break
}
}
let deviceImportResult: {
hostids?: string[];
error?: any;
} = await zabbixAPI.requestByPath("host.create", new ParsedArgs(
{
host: device.deviceKey,
name: device.name,
location: device.location,
templateids: [
await HostImporter.getTemplateIdForDeviceType(
device.deviceType, zabbixAuthToken, cookie)],
hostgroupids: groupids
}
), zabbixAuthToken, cookie)
if (deviceImportResult?.hostids?.length) {
result.push({
deviceKey: device.deviceKey,
hostid: deviceImportResult.hostids[0],
})
} else {
result.push({
deviceKey: device.deviceKey,
message: `Unable to import deviceKey=${device.deviceKey}: ${deviceImportResult.error.message}`,
error: deviceImportResult.error
})
}
}
return result
}
private static async getTemplateIdForDeviceType(deviceType: String, zabbixAuthToken?: string, cookie?: string): Promise<number | undefined> {
let result: number | undefined
let templates = await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ParsedArgs(
{
tag_deviceType: deviceType
}
));
if (templates?.length) {
result = Number(templates[0].templateid)
} else {
logger.error(`Unable to get template for deviceType=${deviceType}: ${result}`)
}
return result
}
}

5
src/index.ts Normal file
View file

@ -0,0 +1,5 @@
import {startAPi} from "./api/start.js";
startAPi()

64
src/logging/logger.ts Normal file
View file

@ -0,0 +1,64 @@
export enum Loglevel {
ERROR="ERROR", WARN="WARN", INFO="INFO", TRACE="TRACE", DEBUG="DEBUG"
}
export class Logger {
public levels:Set<Loglevel> | undefined = undefined
public logMqtt = true
constructor() {
this.readEnvironmentLogLevel()
}
readEnvironmentLogLevel() {
const levels = process.env.LOG_LEVELS
if (levels) {
const enumLevels = levels.split(",").map(v=> Loglevel[v as keyof typeof Loglevel])
this.levels = new Set<Loglevel>(enumLevels)
}
}
public trace(...data: any[]) {
if (!this.levels || this.levels.has(Loglevel.TRACE)) {
console.log(...data)
if (this.logMqtt) {
// TODO Push to mqtt TEST_STATS_LOG_TOPIC topic
}
}
}
public warn(...data: any[]) {
if (!this.levels || this.levels.has(Loglevel.WARN)) {
console.warn(...data)
if (this.logMqtt) {
// TODO Push to mqtt TEST_STATS_LOG_TOPIC topic
}
}
}
public info(...data: any[]) {
if (!this.levels || this.levels.has(Loglevel.INFO)) {
console.log(...data)
if (this.logMqtt) {
// TODO Push to mqtt TEST_STATS_LOG_TOPIC topic
}
}
}
public error(...data: any[]) {
if (!this.levels || this.levels.has(Loglevel.ERROR)) {
console.error(...data)
if (this.logMqtt) {
// TODO Push to mqtt TEST_STATS_LOG_TOPIC topic
}
}
}
public debug(...data: any[]) {
if (!this.levels || this.levels.has(Loglevel.DEBUG)) {
console.debug(...data)
if (this.logMqtt) {
// TODO Push to mqtt TEST_STATS_LOG_TOPIC topic
}
}
}
}
export const logger = new Logger()

View file

@ -0,0 +1,70 @@
// Zabbix value enum mappings
export enum DeviceCommunicationType {
ZABBIX_AGENT = "0",
ZABBIX_TRAP = "2",
SIMPLE_CHECK = "3",
ZABBIX_INTERNAL_ITEM = "5",
ZABBIX_AGENT_ACTIVE = "7",
DATABASE_MONITOR = "11",
IPMI_AGENT = "12",
SIMULATOR_CALCULATED = "15",
JMX_AGENT = "16",
SNMP_TRAP = "17",
DEPENDANT_ITEM = "18",
HTTP_AGENT = "19",
SNMP_AGENT = "20",
SIMULATOR_JAVASCRIPT = "21",
}
export enum DeviceStatus {
DISABLED = "0",
ENABLED = "1"
}
export enum StorageItemType {
Float = 0,
Int = 3,
Text = 4,
}
export enum ApiErrorCode {
OK = 0,
ZABBIX_ERROR = 1000,
ZABBIX_NO_ITEM_PUSH_ITEM = 1001,
ZABBIX_HOST_NOT_FOUND = 1002,
ZABBIX_ITEM_NOT_FOUND = 1003,
ZABBIX_HISTORY_NOT_FOUND = 1004,
ZABBIX_TEMPLATE_NOT_FOUND = 1005,
ZABBIX_SCRIPT_NOT_FOUND = 1006,
ZABBIX_HISTORY_PUSH_FAILED = 1007,
ZABBIX_TEMPLATEGROUP_NOT_FOUND= 1008,
ZABBIX_HOSTGROUP_NOT_FOUND = 1009,
ZABBIX_MULTIPLE_USERGROUPS_FOUND = 1010,
ZABBIX_MODULE_NOT_FOUND= 1011,
VALIDATION_ERROR = 2001,
PERMISSION_ERROR = 2002,
}
export enum ApiErrorMessage {
OK = "",
ZABBIX_NO_TRAPPER_ITEMS_FOR_PUSHING_VALUES_FOUND = "Unable to push value to history, didn't find corresponding trapper item",
ZABBIX_ITEM_NOT_FOUND = "Find a zabbix item with corresponding id",
UPDATE_SKIPPED_NO_CHANGES = "Update skipped - nothing changed",
ZABBIX_REQUEST_EXCEPTION = "Unable to access zabbix api",
ZABBIX_UNABLE_TO_PUSH_VALUE = "Unable to push value to history",
ZABBIX_UNABLE_TO_RETRIEVE_ITEMS_ACCORDING_TO_FILTER = "Unable to retrieve items for specified filter",
ZABBIX_UNABLE_TO_RETRIEVE_HISTORY = "Unable to retrieve history"
}
export const enum Permission {
Read = "2",
ReadWrite = "3",
Deny = "0"
}
export const enum PermissionNumber {
Read = 2,
ReadWrite = 3,
Deny = 0
}

29
tsconfig.json Normal file
View file

@ -0,0 +1,29 @@
{
"ts-node": {
"transpileOnly": true,
"files": true,
"esm": true,
"compilerOptions": {
"module": "esnext"
}
},
"compilerOptions": {
"rootDirs": ["src"],
"outDir": "dist",
"lib": ["es2020"],
"target": "es2022",
"module": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["node", "jest"],
"skipLibCheck": true,
"strict": true,
"strictPropertyInitialization": false,
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
},
"include": ["./src/**/*", "./src/*"],
"files": ["node_modules/jest-expect-message/types/index.d.ts"]
}

8
zabbix-graphql-api.iml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>