Initial commit: Extract base Zabbix GraphQl - API functionality from VCR Project and add dynamic schema samples
This commit is contained in:
commit
92ffe71684
42 changed files with 4234 additions and 0 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal 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
9
.idea/.gitignore
generated
vendored
Normal 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
6
.idea/compiler.xml
generated
Normal 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
11
.idea/go.imports.xml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
15
.idea/runConfigurations/codegen.xml
generated
Normal 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
15
.idea/runConfigurations/compile.xml
generated
Normal 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
17
.idea/runConfigurations/index_ts.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
19
.idea/workspace.xml
generated
Normal 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">{
|
||||
"keyToString": {
|
||||
"go.import.settings.migrated": "true",
|
||||
"settings.editor.selected.configurable": "ssh.settings"
|
||||
}
|
||||
}</component>
|
||||
</project>
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal 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
23
codegen.ts
Normal 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;
|
||||
74
extensions/display_devices.graphql
Normal file
74
extensions/display_devices.graphql
Normal 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
24
jest.config.js
Normal 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
53
package.json
Normal 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
625
schema.graphql
Normal 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
194
src/api/resolver_helpers.ts
Normal 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
257
src/api/resolvers.ts
Normal 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
59
src/api/schema.ts
Normal 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
98
src/api/start.ts
Normal 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
8
src/common_utils.ts
Normal 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 };
|
||||
}
|
||||
115
src/datasources/zabbix-api.ts
Normal file
115
src/datasources/zabbix-api.ts
Normal 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 || "")
|
||||
128
src/datasources/zabbix-history.ts
Normal file
128
src/datasources/zabbix-history.ts
Normal 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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
119
src/datasources/zabbix-hostgroups.ts
Normal file
119
src/datasources/zabbix-hostgroups.ts
Normal 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
|
||||
}
|
||||
}
|
||||
303
src/datasources/zabbix-hosts.ts
Normal file
303
src/datasources/zabbix-hosts.ts
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
192
src/datasources/zabbix-items.ts
Normal file
192
src/datasources/zabbix-items.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/datasources/zabbix-module.ts
Normal file
16
src/datasources/zabbix-module.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
66
src/datasources/zabbix-permissions.ts
Normal file
66
src/datasources/zabbix-permissions.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
530
src/datasources/zabbix-request.ts
Normal file
530
src/datasources/zabbix-request.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
89
src/datasources/zabbix-script.ts
Normal file
89
src/datasources/zabbix-script.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
src/datasources/zabbix-templates.ts
Normal file
32
src/datasources/zabbix-templates.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
330
src/datasources/zabbix-usergroups.ts
Normal file
330
src/datasources/zabbix-usergroups.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/datasources/zabbix-userroles.ts
Normal file
175
src/datasources/zabbix-userroles.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
118
src/execution/host_exporter.ts
Normal file
118
src/execution/host_exporter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/execution/host_importer.ts
Normal file
158
src/execution/host_importer.ts
Normal 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
5
src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import {startAPi} from "./api/start.js";
|
||||
|
||||
startAPi()
|
||||
|
||||
|
||||
64
src/logging/logger.ts
Normal file
64
src/logging/logger.ts
Normal 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()
|
||||
70
src/model/model_enum_values.ts
Normal file
70
src/model/model_enum_values.ts
Normal 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
29
tsconfig.json
Normal 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
8
zabbix-graphql-api.iml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue