From a5f8eb02bbc431e4a1dd9c2d550367202071f0e5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 2 Jun 2020 15:48:33 +0300 Subject: [PATCH] Zabbix API refactoring --- pkg/datasource.go | 29 +-- pkg/{zabbix_api.go => zabbix.go} | 64 ++++++- pkg/zabbix_api_core.go | 162 ----------------- pkg/{zabbix_api_test.go => zabbix_test.go} | 4 +- pkg/zabbixapi/models.go | 8 + pkg/zabbixapi/zabbix_api.go | 199 +++++++++++++++++++++ 6 files changed, 286 insertions(+), 180 deletions(-) rename pkg/{zabbix_api.go => zabbix.go} (87%) delete mode 100644 pkg/zabbix_api_core.go rename pkg/{zabbix_api_test.go => zabbix_test.go} (99%) create mode 100644 pkg/zabbixapi/models.go create mode 100644 pkg/zabbixapi/zabbix_api.go diff --git a/pkg/datasource.go b/pkg/datasource.go index b59401b..e225f0d 100644 --- a/pkg/datasource.go +++ b/pkg/datasource.go @@ -12,6 +12,7 @@ import ( "time" "github.com/alexanderzobnin/grafana-zabbix/pkg/gtime" + "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" hclog "github.com/hashicorp/go-hclog" plugin "github.com/hashicorp/go-plugin" @@ -37,6 +38,7 @@ type ZabbixDatasource struct { type ZabbixDatasourceInstance struct { url *url.URL authToken string + zabbixAPI *zabbixapi.ZabbixAPI dsInfo *backend.DataSourceInstanceSettings Settings *ZabbixDatasourceSettings queryCache *Cache @@ -44,6 +46,17 @@ type ZabbixDatasourceInstance struct { logger log.Logger } +// NewZabbixDatasource returns new datasource instance. +func (ds *ZabbixDatasource) NewZabbixDatasource(dsInfo *backend.DataSourceInstanceSettings) (*ZabbixDatasourceInstance, error) { + dsInstance, err := newZabbixDatasource(dsInfo) + if err != nil { + return nil, err + } + + dsInstance.logger = ds.logger + return dsInstance, nil +} + // CheckHealth checks if the plugin is running properly func (ds *ZabbixDatasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { res := &backend.CheckHealthResult{} @@ -104,16 +117,6 @@ func (ds *ZabbixDatasource) QueryData(ctx context.Context, req *backend.QueryDat // func (p *ZabbixPlugin) GetDatasourceById(datasourceId int64) (*ZabbixDatasourceInstance, error) { // } -func (ds *ZabbixDatasource) NewZabbixDatasource(dsInfo *backend.DataSourceInstanceSettings) (*ZabbixDatasourceInstance, error) { - dsInstance, err := newZabbixDatasource(dsInfo) - if err != nil { - return nil, err - } - - dsInstance.logger = ds.logger - return dsInstance, nil -} - // newZabbixDatasource returns an initialized ZabbixDatasource func newZabbixDatasource(dsInfo *backend.DataSourceInstanceSettings) (*ZabbixDatasourceInstance, error) { zabbixURLStr := dsInfo.URL @@ -122,12 +125,18 @@ func newZabbixDatasource(dsInfo *backend.DataSourceInstanceSettings) (*ZabbixDat return nil, err } + zabbixAPI, err := zabbixapi.New(dsInfo.URL) + if err != nil { + return nil, err + } + zabbixSettings, err := readZabbixSettings(dsInfo) if err != nil { return nil, err } return &ZabbixDatasourceInstance{ + zabbixAPI: zabbixAPI, url: zabbixURL, dsInfo: dsInfo, Settings: zabbixSettings, diff --git a/pkg/zabbix_api.go b/pkg/zabbix.go similarity index 87% rename from pkg/zabbix_api.go rename to pkg/zabbix.go index 3ed4bbe..e37fb1b 100644 --- a/pkg/zabbix_api.go +++ b/pkg/zabbix.go @@ -7,6 +7,7 @@ import ( "time" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" + "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" simplejson "github.com/bitly/go-simplejson" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -14,10 +15,11 @@ import ( ) var NotCachedMethods = map[string]bool{ - "history.get": true, - "trend.get": true, - "problem.get": true, - "trigger.get": true, + "history.get": true, + "trend.get": true, + "problem.get": true, + "trigger.get": true, + "apiinfo.version": true, } // ZabbixQuery handles query requests to Zabbix @@ -48,7 +50,7 @@ func (ds *ZabbixDatasourceInstance) ZabbixQuery(ctx context.Context, apiReq *Zab return resultJson, nil } -// ZabbixAPIQuery handles query requests to Zabbix +// ZabbixAPIQuery handles query requests to Zabbix API func (ds *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, apiReq *ZabbixAPIRequest) (*ZabbixAPIResourceResponse, error) { resultJson, err := ds.ZabbixQuery(ctx, apiReq) if err != nil { @@ -65,7 +67,7 @@ func (ds *ZabbixDatasourceInstance) TestConnection(ctx context.Context) (string, return "", err } - response, err := ds.ZabbixAPIRequest(ctx, "apiinfo.version", ZabbixAPIParams{}, "") + response, err := ds.ZabbixRequest(ctx, "apiinfo.version", ZabbixAPIParams{}) if err != nil { return "", err } @@ -76,6 +78,56 @@ func (ds *ZabbixDatasourceInstance) TestConnection(ctx context.Context) (string, return string(resultByte), nil } +// ZabbixRequest checks authentication and makes a request to the Zabbix API +func (ds *ZabbixDatasourceInstance) ZabbixRequest(ctx context.Context, method string, params ZabbixAPIParams) (*simplejson.Json, error) { + ds.logger.Debug("Invoke Zabbix API request", "ds", ds.dsInfo.Name, "method", method) + var result *simplejson.Json + var err error + + // Skip auth for methods that are not required it + if method == "apiinfo.version" { + return ds.zabbixAPI.RequestUnauthenticated(ctx, method, params) + } + + result, err = ds.zabbixAPI.Request(ctx, method, params) + if err == zabbixapi.ErrNotAuthenticated { + err = ds.login(ctx) + if err != nil { + return nil, err + } + return ds.ZabbixRequest(ctx, method, params) + } else if err != nil { + return nil, err + } + + return result, err +} + +func (ds *ZabbixDatasourceInstance) login(ctx context.Context) error { + jsonData, err := simplejson.NewJson(ds.dsInfo.JSONData) + if err != nil { + return err + } + + zabbixLogin := jsonData.Get("username").MustString() + var zabbixPassword string + if securePassword, exists := ds.dsInfo.DecryptedSecureJSONData["password"]; exists { + zabbixPassword = securePassword + } else { + // Fallback + zabbixPassword = jsonData.Get("password").MustString() + } + + err = ds.zabbixAPI.Authenticate(ctx, zabbixLogin, zabbixPassword) + if err != nil { + ds.logger.Error("Zabbix authentication error", "error", err) + return err + } + ds.logger.Debug("Successfully authenticated", "url", ds.zabbixAPI.GetUrl().String(), "user", zabbixLogin) + + return nil +} + func (ds *ZabbixDatasourceInstance) queryNumericItems(ctx context.Context, query *QueryModel) (*data.Frame, error) { groupFilter := query.Group.Filter hostFilter := query.Host.Filter diff --git a/pkg/zabbix_api_core.go b/pkg/zabbix_api_core.go deleted file mode 100644 index 848e27f..0000000 --- a/pkg/zabbix_api_core.go +++ /dev/null @@ -1,162 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - - simplejson "github.com/bitly/go-simplejson" - "golang.org/x/net/context/ctxhttp" -) - -// ZabbixRequest checks authentication and makes a request to the Zabbix API -func (ds *ZabbixDatasourceInstance) ZabbixRequest(ctx context.Context, method string, params ZabbixAPIParams) (*simplejson.Json, error) { - ds.logger.Debug("Invoke Zabbix API request", "ds", ds.dsInfo.Name, "method", method) - 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, "") - } - - for attempt := 0; attempt <= 3; attempt++ { - if ds.authToken == "" { - // Authenticate - err = ds.loginWithDs(ctx) - if err != nil { - return nil, err - } - } - result, err = ds.ZabbixAPIRequest(ctx, method, params, ds.authToken) - if err == nil || (err != nil && !isNotAuthorized(err.Error())) { - break - } else { - ds.authToken = "" - } - } - return result, err -} - -func (ds *ZabbixDatasourceInstance) loginWithDs(ctx context.Context) error { - jsonDataStr := ds.dsInfo.JSONData - jsonData, err := simplejson.NewJson(jsonDataStr) - if err != nil { - return err - } - - zabbixLogin := jsonData.Get("username").MustString() - var zabbixPassword string - if securePassword, exists := ds.dsInfo.DecryptedSecureJSONData["password"]; exists { - zabbixPassword = securePassword - } else { - zabbixPassword = jsonData.Get("password").MustString() - } - - auth, err := ds.login(ctx, zabbixLogin, zabbixPassword) - if err != nil { - ds.logger.Error("Authentication error", "error", err) - ds.authToken = "" - return err - } - ds.logger.Debug("Successfully authenticated", "url", ds.url, "user", zabbixLogin) - ds.authToken = auth - - return nil -} - -func (ds *ZabbixDatasourceInstance) login(ctx context.Context, username string, password string) (string, error) { - params := ZabbixAPIParams{ - "user": username, - "password": password, - } - auth, err := ds.ZabbixAPIRequest(ctx, "user.login", params, "") - if err != nil { - return "", err - } - - return auth.MustString(), nil -} - -func (ds *ZabbixDatasourceInstance) ZabbixAPIRequest(ctx context.Context, method string, params ZabbixAPIParams, auth string) (*simplejson.Json, error) { - apiRequest := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 2, - "method": method, - "params": params, - } - - if auth != "" { - apiRequest["auth"] = auth - } - - reqBodyJSON, err := json.Marshal(apiRequest) - if err != nil { - return nil, err - } - - var body io.Reader - body = bytes.NewReader(reqBodyJSON) - rc, ok := body.(io.ReadCloser) - if !ok && body != nil { - rc = ioutil.NopCloser(body) - } - - req := &http.Request{ - Method: "POST", - URL: ds.url, - Header: map[string][]string{ - "Content-Type": {"application/json"}, - }, - Body: rc, - } - - response, err := makeHTTPRequest(ctx, ds.httpClient, req) - if err != nil { - return nil, err - } - - return handleAPIResult(response) -} - -func handleAPIResult(response []byte) (*simplejson.Json, error) { - jsonResp, err := simplejson.NewJson([]byte(response)) - if err != nil { - return nil, err - } - if errJSON, isError := jsonResp.CheckGet("error"); isError { - errMessage := fmt.Sprintf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString()) - return nil, errors.New(errMessage) - } - jsonResult := jsonResp.Get("result") - return jsonResult, nil -} - -func makeHTTPRequest(ctx context.Context, httpClient *http.Client, req *http.Request) ([]byte, error) { - res, err := ctxhttp.Do(ctx, httpClient, req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("invalid status code. status: %v", res.Status) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, err - } - return body, nil -} - -func isNotAuthorized(message string) bool { - return message == "Session terminated, re-login, please." || - message == "Not authorised." || - message == "Not authorized." -} diff --git a/pkg/zabbix_api_test.go b/pkg/zabbix_test.go similarity index 99% rename from pkg/zabbix_api_test.go rename to pkg/zabbix_test.go index 511becc..d4aeef7 100644 --- a/pkg/zabbix_api_test.go +++ b/pkg/zabbix_test.go @@ -123,7 +123,7 @@ func TestLoginError(t *testing.T) { func TestLoginWithDs(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) - err := zabbixDatasource.loginWithDs(context.Background()) + err := zabbixDatasource.login(context.Background()) assert.Equal(t, "sampleResult", zabbixDatasource.authToken) assert.Nil(t, err) @@ -132,7 +132,7 @@ func TestLoginWithDs(t *testing.T) { func TestLoginWithDsError(t *testing.T) { errResponse := `{"error":{"code":-32500,"message":"Application error.","data":"Login name or password is incorrect."}}` zabbixDatasource := mockZabbixDataSource(errResponse, 200) - err := zabbixDatasource.loginWithDs(context.Background()) + err := zabbixDatasource.login(context.Background()) assert.Equal(t, "", zabbixDatasource.authToken) assert.NotNil(t, err) diff --git a/pkg/zabbixapi/models.go b/pkg/zabbixapi/models.go new file mode 100644 index 0000000..61f9f02 --- /dev/null +++ b/pkg/zabbixapi/models.go @@ -0,0 +1,8 @@ +package zabbixapi + +type ZabbixAPIRequest struct { + Method string `json:"method"` + Params ZabbixAPIParams `json:"params,omitempty"` +} + +type ZabbixAPIParams = map[string]interface{} diff --git a/pkg/zabbixapi/zabbix_api.go b/pkg/zabbixapi/zabbix_api.go new file mode 100644 index 0000000..3912f94 --- /dev/null +++ b/pkg/zabbixapi/zabbix_api.go @@ -0,0 +1,199 @@ +package zabbixapi + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "time" + + "github.com/bitly/go-simplejson" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "golang.org/x/net/context/ctxhttp" +) + +var ( + ErrNotAuthenticated = errors.New("zabbix api: not authenticated") +) + +type ZabbixAPI struct { + url *url.URL + httpClient *http.Client + logger log.Logger + auth string +} + +func newHttpClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + }, + Timeout: time.Duration(time.Second * 30), + } +} + +// New returns new ZabbixAPI instance initialized with given URL or error. +func New(api_url string) (*ZabbixAPI, error) { + apiLogger := log.New() + zabbixURL, err := url.Parse(api_url) + if err != nil { + return nil, err + } + + return &ZabbixAPI{ + url: zabbixURL, + logger: apiLogger, + httpClient: newHttpClient(), + }, nil +} + +// GetUrl gets new API URL +func (api *ZabbixAPI) GetUrl() *url.URL { + return api.url +} + +// SetUrl sets new API URL +func (api *ZabbixAPI) SetUrl(api_url string) error { + zabbixURL, err := url.Parse(api_url) + if err != nil { + return err + } + + api.url = zabbixURL + return nil +} + +// SetAuth sets API authentication token +func (api *ZabbixAPI) SetAuth(auth string) { + api.auth = auth +} + +// Request performs API request +func (api *ZabbixAPI) Request(ctx context.Context, method string, params ZabbixAPIParams) (*simplejson.Json, error) { + if api.auth == "" { + return nil, ErrNotAuthenticated + } + + return api.request(ctx, method, params, api.auth) +} + +// Request performs API request without authentication token +func (api *ZabbixAPI) RequestUnauthenticated(ctx context.Context, method string, params ZabbixAPIParams) (*simplejson.Json, error) { + return api.request(ctx, method, params, "") +} + +func (api *ZabbixAPI) request(ctx context.Context, method string, params ZabbixAPIParams, auth string) (*simplejson.Json, error) { + apiRequest := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 2, + "method": method, + "params": params, + } + + if auth != "" { + apiRequest["auth"] = auth + } + + reqBodyJSON, err := json.Marshal(apiRequest) + if err != nil { + return nil, err + } + + var body io.Reader + body = bytes.NewReader(reqBodyJSON) + rc, ok := body.(io.ReadCloser) + if !ok && body != nil { + rc = ioutil.NopCloser(body) + } + + req := &http.Request{ + Method: "POST", + URL: api.url, + Header: map[string][]string{ + "Content-Type": {"application/json"}, + }, + Body: rc, + } + + response, err := makeHTTPRequest(ctx, api.httpClient, req) + if err != nil { + return nil, err + } + + return handleAPIResult(response) +} + +// Login performs API authentication and returns authentication token. +func (api *ZabbixAPI) Login(ctx context.Context, username string, password string) (string, error) { + params := ZabbixAPIParams{ + "user": username, + "password": password, + } + + auth, err := api.request(ctx, "user.login", params, "") + if err != nil { + return "", err + } + + return auth.MustString(), nil +} + +// Authenticate performs API authentication and sets authentication token. +func (api *ZabbixAPI) Authenticate(ctx context.Context, username string, password string) error { + auth, err := api.Login(ctx, username, password) + if err != nil { + return err + } + + api.SetAuth(auth) + return nil +} + +func handleAPIResult(response []byte) (*simplejson.Json, error) { + jsonResp, err := simplejson.NewJson([]byte(response)) + if err != nil { + return nil, err + } + if errJSON, isError := jsonResp.CheckGet("error"); isError { + errMessage := fmt.Sprintf("%s %s", errJSON.Get("message").MustString(), errJSON.Get("data").MustString()) + return nil, errors.New(errMessage) + } + jsonResult := jsonResp.Get("result") + return jsonResult, nil +} + +func makeHTTPRequest(ctx context.Context, httpClient *http.Client, req *http.Request) ([]byte, error) { + res, err := ctxhttp.Do(ctx, httpClient, req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("invalid status code. status: %v", res.Status) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + return body, nil +}