From 9f344cb867ac13477322ad91d71a4dd974744d9f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 29 May 2020 12:31:43 +0300 Subject: [PATCH] Refactor API calls --- pkg/datasource.go | 6 + pkg/models.go | 4 + pkg/resource_handler.go | 5 +- pkg/zabbix_api.go | 5 +- pkg/zabbix_api_core.go | 5 + src/datasource-zabbix/datasource.ts | 13 +- .../zabbix_api/zabbixAPIConnector.ts | 168 +++++------------- .../connectors/zabbix_api/zabbixAPICore.ts | 105 ----------- src/datasource-zabbix/zabbix/types.ts | 1 - src/datasource-zabbix/zabbix/zabbix.ts | 8 +- 10 files changed, 66 insertions(+), 254 deletions(-) delete mode 100644 src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts diff --git a/pkg/datasource.go b/pkg/datasource.go index 1521162..54b431e 100644 --- a/pkg/datasource.go +++ b/pkg/datasource.go @@ -170,6 +170,12 @@ func GetQueryType(tsdbReq *datasource.DatasourceRequest) (string, error) { // }, nil // } +func BuildAPIResponse(responseData *interface{}) (*ZabbixAPIResourceResponse, error) { + return &ZabbixAPIResourceResponse{ + Result: *responseData, + }, nil +} + // BuildResponse transforms a Zabbix API response to a DatasourceResponse func BuildResponse(responseData interface{}) (*datasource.DatasourceResponse, error) { jsonBytes, err := json.Marshal(responseData) diff --git a/pkg/models.go b/pkg/models.go index ee434f0..1e86583 100644 --- a/pkg/models.go +++ b/pkg/models.go @@ -115,6 +115,10 @@ type ZabbixAPIRequest struct { Params map[string]interface{} `json:"params,omitempty"` } +type ZabbixAPIResourceResponse struct { + Result interface{} `json:"result,omitempty"` +} + func (r *ZabbixAPIRequest) String() string { jsonRequest, _ := json.Marshal(r.Params) return r.Method + string(jsonRequest) diff --git a/pkg/resource_handler.go b/pkg/resource_handler.go index 4d3d31c..691bce5 100644 --- a/pkg/resource_handler.go +++ b/pkg/resource_handler.go @@ -32,16 +32,17 @@ func (ds *ZabbixDatasource) zabbixAPIHandler(rw http.ResponseWriter, req *http.R var reqData ZabbixAPIResourceRequest err = json.Unmarshal(body, &reqData) if err != nil { + ds.logger.Error("Cannot unmarshal request", "error", err.Error()) WriteError(rw, http.StatusInternalServerError, err) return } pluginCxt := httpadapter.PluginConfigFromContext(req.Context()) - ds.logger.Debug("Received Zabbix API call", "ds", pluginCxt.DataSourceInstanceSettings.Name) dsInstance, err := ds.GetDatasource(pluginCxt.OrgID, pluginCxt.DataSourceInstanceSettings) ds.logger.Debug("Data source found", "ds", dsInstance.dsInfo.Name) + ds.logger.Debug("Invoke Zabbix API call", "ds", pluginCxt.DataSourceInstanceSettings.Name, "method", reqData.Method) apiReq := &ZabbixAPIRequest{Method: reqData.Method, Params: reqData.Params} result, err := dsInstance.ZabbixAPIQuery(req.Context(), apiReq) if err != nil { @@ -52,7 +53,7 @@ func (ds *ZabbixDatasource) zabbixAPIHandler(rw http.ResponseWriter, req *http.R WriteResponse(rw, result) } -func WriteResponse(rw http.ResponseWriter, result *interface{}) { +func WriteResponse(rw http.ResponseWriter, result *ZabbixAPIResourceResponse) { resultJson, err := json.Marshal(*result) if err != nil { WriteError(rw, http.StatusInternalServerError, err) diff --git a/pkg/zabbix_api.go b/pkg/zabbix_api.go index 78d959f..f1281cf 100644 --- a/pkg/zabbix_api.go +++ b/pkg/zabbix_api.go @@ -25,7 +25,7 @@ type FunctionCategories struct { } // ZabbixAPIQuery handles query requests to Zabbix -func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, apiReq *ZabbixAPIRequest) (*interface{}, error) { +func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, apiReq *ZabbixAPIRequest) (*ZabbixAPIResourceResponse, error) { var result interface{} var err error var queryExistInCache bool @@ -41,8 +41,7 @@ func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, } } - // return BuildResponse(result) - return &result, nil + return BuildAPIResponse(&result) } func (ds *ZabbixDatasourceInstance) ZabbixAPIQueryOld(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { diff --git a/pkg/zabbix_api_core.go b/pkg/zabbix_api_core.go index 379e9d0..69389eb 100644 --- a/pkg/zabbix_api_core.go +++ b/pkg/zabbix_api_core.go @@ -68,6 +68,11 @@ func (ds *ZabbixDatasourceInstance) ZabbixRequest(ctx context.Context, method st var result *simplejson.Json var err error + // Skip auth for methods that are not required it + if method == "apiinfo.version" { + return ds.ZabbixAPIRequest(ctx, method, params, ds.authToken) + } + for attempt := 0; attempt <= 3; attempt++ { if ds.authToken == "" { // Authenticate diff --git a/src/datasource-zabbix/datasource.ts b/src/datasource-zabbix/datasource.ts index 6746ad1..fc60dc7 100644 --- a/src/datasource-zabbix/datasource.ts +++ b/src/datasource-zabbix/datasource.ts @@ -10,19 +10,16 @@ import dataProcessor from './dataProcessor'; import responseHandler from './responseHandler'; import problemsHandler from './problemsHandler'; import { Zabbix } from './zabbix/zabbix'; -import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore'; +import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPIConnector'; import { VariableQueryTypes, ShowProblemTypes } from './types'; import { getBackendSrv } from '@grafana/runtime'; import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; export class ZabbixDatasource extends DataSourceApi { name: string; - url: string; basicAuth: any; withCredentials: any; - username: string; - password: string; trends: boolean; trendsFrom: string; trendsRange: string; @@ -56,16 +53,11 @@ export class ZabbixDatasource extends DataSourceApi { // General data source settings this.datasourceId = instanceSettings.id; this.name = instanceSettings.name; - this.url = instanceSettings.url; this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; const jsonData = migrations.migrateDSConfig(instanceSettings.jsonData); - // Zabbix API credentials - this.username = jsonData.username; - this.password = jsonData.password; - // Use trends instead history since specified time this.trends = jsonData.trends; this.trendsFrom = jsonData.trendsFrom || '7d'; @@ -90,9 +82,6 @@ export class ZabbixDatasource extends DataSourceApi { this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy; const zabbixOptions = { - url: this.url, - username: this.username, - password: this.password, basicAuth: this.basicAuth, withCredentials: this.withCredentials, cacheTTL: this.cacheTTL, diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index 8a53d14..718c539 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -2,10 +2,9 @@ import _ from 'lodash'; import semver from 'semver'; import kbn from 'grafana/app/core/utils/kbn'; import * as utils from '../../../utils'; -import { ZabbixAPICore } from './zabbixAPICore'; -import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; +import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; import { ShowProblemTypes, ZBXProblem } from '../../../types'; -import { JSONRPCRequestParams } from './types'; +import { GFHTTPRequest, JSONRPCError } from './types'; import { getBackendSrv } from '@grafana/runtime'; const DEFAULT_ZABBIX_VERSION = '3.0.0'; @@ -16,39 +15,22 @@ const DEFAULT_ZABBIX_VERSION = '3.0.0'; * Wraps API calls and provides high-level methods. */ export class ZabbixAPIConnector { - url: string; - username: string; - password: string; - auth: string; + backendAPIUrl: string; requestOptions: { basicAuth: any; withCredentials: boolean; }; - loginPromise: Promise; - loginErrorCount: number; - maxLoginAttempts: number; - zabbixAPICore: ZabbixAPICore; getTrend: (items: any, timeFrom: any, timeTill: any) => Promise; version: string; getVersionPromise: Promise; datasourceId: number; - constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) { - this.url = api_url; - this.username = username; - this.password = password; - this.auth = ''; + constructor(basicAuth: any, withCredentials: boolean, datasourceId: number) { + this.datasourceId = datasourceId; + this.backendAPIUrl = `/api/datasources/${this.datasourceId}/resources/zabbix-api`; this.requestOptions = { basicAuth: basicAuth, withCredentials: withCredentials }; - this.datasourceId = datasourceId; - - this.loginPromise = null; - this.loginErrorCount = 0; - this.maxLoginAttempts = 3; - - this.zabbixAPICore = new ZabbixAPICore(); - this.getTrend = this.getTrend_ZBXNEXT1193; //getTrend = getTrend_30; @@ -59,113 +41,42 @@ export class ZabbixAPIConnector { // Core method wrappers // ////////////////////////// - request(method, params) { - return this.tsdbRequest(method, params).then(response => { - // const result = this.handleTsdbResponse(response); - const result = this.handleZabbixAPIResourceResponse(response); - - return result; + request(method: string, params?: any) { + return this.backendAPIRequest(method, params).then(response => { + return response?.data?.result; }); } - tsdbRequest(method, params) { - const tsdbRequestData = { - queries: [{ - datasourceId: this.datasourceId, - queryType: 'zabbixAPI', - target: { - method, - params, - }, - }], - }; - - return getBackendSrv().datasourceRequest({ - url: `/api/datasources/${this.datasourceId}/resources/zabbix-api`, + backendAPIRequest(method: string, params: any = {}) { + const requestOptions: GFHTTPRequest = { + url: this.backendAPIUrl, method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, data: { datasourceId: this.datasourceId, method, params, }, - }); + }; - // return getBackendSrv().datasourceRequest({ - // url: '/api/tsdb/query', - // method: 'POST', - // data: tsdbRequestData - // }); - } - - _request(method: string, params: JSONRPCRequestParams): Promise { - if (!this.version) { - return this.initVersion().then(() => this.request(method, params)); + // Set request options for basic auth + if (this.requestOptions.basicAuth || this.requestOptions.withCredentials) { + requestOptions.withCredentials = true; + } + if (this.requestOptions.basicAuth) { + requestOptions.headers.Authorization = this.requestOptions.basicAuth; } - return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth) - .catch(error => { - if (isNotInitialized(error.data)) { - // If API not initialized yet (auth is empty), login first - return this.loginOnce() - .then(() => this.request(method, params)); - } else if (isNotAuthorized(error.data)) { - // Handle auth errors - this.loginErrorCount++; - if (this.loginErrorCount > this.maxLoginAttempts) { - this.loginErrorCount = 0; - return Promise.resolve(); - } else { - return this.loginOnce() - .then(() => this.request(method, params)); - } - } else { - return Promise.reject(error); - } - }); - } - - handleTsdbResponse(response) { - if (!response || !response.data || !response.data.results) { - return []; - } - - return response.data.results['zabbixAPI'].meta; - } - - handleZabbixAPIResourceResponse(response) { - return response?.data; - } - - /** - * When API unauthenticated or auth token expired each request produce login() - * call. But auth token is common to all requests. This function wraps login() method - * and call it once. If login() already called just wait for it (return its promise). - */ - loginOnce(): Promise { - if (!this.loginPromise) { - this.loginPromise = Promise.resolve( - this.login().then(auth => { - this.auth = auth; - this.loginPromise = null; - return auth; - }) - ); - } - return this.loginPromise; - } - - /** - * Get authentication token. - */ - login(): Promise { - return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions); + return getBackendSrv().datasourceRequest(requestOptions); } /** * Get Zabbix API version */ getVersion() { - return this.zabbixAPICore.getVersion(this.url, this.requestOptions); + return this.request('apiinfo.version'); } initVersion(): Promise { @@ -730,18 +641,6 @@ function filterTriggersByAcknowledge(triggers, acknowledged) { } } -function isNotAuthorized(message) { - return ( - message === "Session terminated, re-login, please." || - message === "Not authorised." || - message === "Not authorized." - ); -} - -function isNotInitialized(message) { - return message === "Not initialized"; -} - function getSLAInterval(intervalMs) { // Too many intervals may cause significant load on the database, so decrease number of resulting points const resolutionRatio = 100; @@ -767,3 +666,22 @@ function buildSLAIntervals(timeRange, interval) { return intervals; } + +// Define zabbix API exception type +export class ZabbixAPIError { + code: number; + name: string; + data: string; + message: string; + + constructor(error: JSONRPCError) { + this.code = error.code || null; + this.name = error.message || ""; + this.data = error.data || ""; + this.message = "Zabbix API Error: " + this.name + " " + this.data; + } + + toString() { + return this.name + " " + this.data; + } +} diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts deleted file mode 100644 index d23880b..0000000 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * General Zabbix API methods - */ -import { getBackendSrv } from '@grafana/runtime'; -import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types'; - -export class ZabbixAPICore { - /** - * Request data from Zabbix API - * @return {object} response.result - */ - request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) { - const requestData: JSONRPCRequest = { - jsonrpc: '2.0', - method: method, - params: params, - id: 1 - }; - - if (auth === "") { - // Reject immediately if not authenticated - return Promise.reject(new ZabbixAPIError({data: "Not initialized"})); - } else if (auth) { - // Set auth parameter only if it needed - requestData.auth = auth; - } - - const requestOptions: GFHTTPRequest = { - method: 'POST', - url: api_url, - data: requestData, - headers: { - 'Content-Type': 'application/json' - } - }; - - // Set request options for basic auth - if (options.basicAuth || options.withCredentials) { - requestOptions.withCredentials = true; - } - if (options.basicAuth) { - requestOptions.headers.Authorization = options.basicAuth; - } - - return this.datasourceRequest(requestOptions); - } - - datasourceRequest(requestOptions) { - return getBackendSrv().datasourceRequest(requestOptions) - .then((response: ZabbixRequestResponse) => { - if (!response?.data) { - return Promise.reject(new ZabbixAPIError({data: "General Error, no data"})); - } else if (response?.data.error) { - - // Handle Zabbix API errors - return Promise.reject(new ZabbixAPIError(response.data.error)); - } - - // Success - return response?.data.result; - }); - } - - /** - * Get authentication token. - * @return {string} auth token - */ - login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise { - const params = { - user: username, - password: password - }; - return this.request(api_url, 'user.login', params, options, null); - } - - /** - * Get Zabbix API version - * Matches the version of Zabbix starting from Zabbix 2.0.4 - */ - getVersion(api_url: string, options: GFRequestOptions): Promise { - return this.request(api_url, 'apiinfo.version', [], options).catch(err => { - console.error(err); - return undefined; - }); - } -} - -// Define zabbix API exception type -export class ZabbixAPIError { - code: number; - name: string; - data: string; - message: string; - - constructor(error: JSONRPCError) { - this.code = error.code || null; - this.name = error.message || ""; - this.data = error.data || ""; - this.message = "Zabbix API Error: " + this.name + " " + this.data; - } - - toString() { - return this.name + " " + this.data; - } -} diff --git a/src/datasource-zabbix/zabbix/types.ts b/src/datasource-zabbix/zabbix/types.ts index dfc53d9..f1ab2dd 100644 --- a/src/datasource-zabbix/zabbix/types.ts +++ b/src/datasource-zabbix/zabbix/types.ts @@ -13,7 +13,6 @@ export interface ZabbixConnector { getExtendedEventData: (eventids) => Promise; getMacros: (hostids: any[]) => Promise; getVersion: () => Promise; - login: () => Promise; getGroups: (groupFilter?) => any; getHosts: (groupFilter?, hostFilter?) => any; diff --git a/src/datasource-zabbix/zabbix/zabbix.ts b/src/datasource-zabbix/zabbix/zabbix.ts index 4847e3e..d086a3c 100644 --- a/src/datasource-zabbix/zabbix/zabbix.ts +++ b/src/datasource-zabbix/zabbix/zabbix.ts @@ -29,7 +29,7 @@ const REQUESTS_TO_CACHE = [ const REQUESTS_TO_BIND = [ 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', - 'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', + 'getAcknowledges', 'getITService', 'getVersion', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', 'getExtendedEventData' ]; @@ -55,13 +55,9 @@ export class Zabbix implements ZabbixConnector { getExtendedEventData: (eventids) => Promise; getMacros: (hostids: any[]) => Promise; getVersion: () => Promise; - login: () => Promise; constructor(options) { const { - url, - username, - password, basicAuth, withCredentials, cacheTTL, @@ -81,7 +77,7 @@ export class Zabbix implements ZabbixConnector { }; this.cachingProxy = new CachingProxy(cacheOptions); - this.zabbixAPI = new ZabbixAPIConnector(url, username, password, basicAuth, withCredentials, datasourceId); + this.zabbixAPI = new ZabbixAPIConnector(basicAuth, withCredentials, datasourceId); this.proxyfyRequests(); this.cacheRequests();