From 7a1743ad417efca23ed6a710e4589df002f85fec Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 14 Jan 2020 17:42:12 +0300 Subject: [PATCH] refactor: simplify API queries --- pkg/datasource.go | 64 +++++------ pkg/datasource_test.go | 15 +-- pkg/models.go | 6 +- pkg/models_test.go | 10 +- pkg/plugin.go | 2 +- pkg/zabbix_api.go | 233 +++-------------------------------------- pkg/zabbix_api_core.go | 209 ++++++++++++++++++++++++++++++++++++ pkg/zabbix_api_test.go | 44 ++++---- 8 files changed, 295 insertions(+), 288 deletions(-) create mode 100644 pkg/zabbix_api_core.go diff --git a/pkg/datasource.go b/pkg/datasource.go index f591eda..7ed0c31 100644 --- a/pkg/datasource.go +++ b/pkg/datasource.go @@ -1,44 +1,41 @@ package main import ( + "context" "encoding/json" "errors" "fmt" - "runtime/debug" simplejson "github.com/bitly/go-simplejson" "github.com/grafana/grafana_plugin_model/go/datasource" hclog "github.com/hashicorp/go-hclog" plugin "github.com/hashicorp/go-plugin" - "golang.org/x/net/context" ) -// ZabbixBackend implements the Grafana backend interface and forwards queries to the ZabbixDatasource -type ZabbixBackend struct { +// ZabbixPlugin implements the Grafana backend interface and forwards queries to the ZabbixDatasource +type ZabbixPlugin struct { plugin.NetRPCUnsupportedPlugin logger hclog.Logger datasourceCache *Cache } -func (b *ZabbixBackend) newZabbixDatasource() *ZabbixDatasource { - ds := NewZabbixDatasource() - ds.logger = b.logger - return ds +func (p *ZabbixPlugin) NewZabbixDatasource(dsInfo *datasource.DatasourceInfo) (*ZabbixDatasource, error) { + ds, err := newZabbixDatasource(dsInfo) + if err != nil { + return nil, err + } + + ds.logger = p.logger + return ds, nil } // Query receives requests from the Grafana backend. Requests are filtered by query type and sent to the // applicable ZabbixDatasource. -func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (resp *datasource.DatasourceResponse, _e error) { - defer func() { - if r := recover(); r != nil { - pErr, _ := r.(error) - b.logger.Error("Fatal error in Zabbix Plugin Backend", "Error", pErr) - b.logger.Error(string(debug.Stack())) - resp = BuildErrorResponse(fmt.Errorf("Unrecoverable error in grafana-zabbix plugin backend")) - } - }() - - zabbixDs := b.getCachedDatasource(tsdbReq) +func (p *ZabbixPlugin) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (resp *datasource.DatasourceResponse, err error) { + zabbixDS, err := p.GetDatasource(tsdbReq) + if err != nil { + return nil, err + } queryType, err := GetQueryType(tsdbReq) if err != nil { @@ -47,11 +44,11 @@ func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.Datasourc switch queryType { case "zabbixAPI": - resp, err = zabbixDs.ZabbixAPIQuery(ctx, tsdbReq) + resp, err = zabbixDS.ZabbixAPIQuery(ctx, tsdbReq) case "query": - resp, err = zabbixDs.queryNumericItems(ctx, tsdbReq) + resp, err = zabbixDS.queryNumericItems(ctx, tsdbReq) case "connectionTest": - resp, err = zabbixDs.TestConnection(ctx, tsdbReq) + resp, err = zabbixDS.TestConnection(ctx, tsdbReq) default: err = errors.New("Query not implemented") return BuildErrorResponse(err), nil @@ -60,23 +57,28 @@ func (b *ZabbixBackend) Query(ctx context.Context, tsdbReq *datasource.Datasourc return } -func (b *ZabbixBackend) getCachedDatasource(tsdbReq *datasource.DatasourceRequest) *ZabbixDatasource { +// GetDatasource Returns cached datasource or creates new one +func (p *ZabbixPlugin) GetDatasource(tsdbReq *datasource.DatasourceRequest) (*ZabbixDatasource, error) { dsInfoHash := HashDatasourceInfo(tsdbReq.GetDatasource()) - if cachedData, ok := b.datasourceCache.Get(dsInfoHash); ok { + if cachedData, ok := p.datasourceCache.Get(dsInfoHash); ok { if cachedDS, ok := cachedData.(*ZabbixDatasource); ok { - return cachedDS + return cachedDS, nil } } - if b.logger.IsDebug() { - dsInfo := tsdbReq.GetDatasource() - b.logger.Debug(fmt.Sprintf("Datasource cache miss (Org %d Id %d '%s' %s)", dsInfo.GetOrgId(), dsInfo.GetId(), dsInfo.GetName(), dsInfoHash)) + dsInfo := tsdbReq.GetDatasource() + if p.logger.IsDebug() { + p.logger.Debug(fmt.Sprintf("Datasource cache miss (Org %d Id %d '%s' %s)", dsInfo.GetOrgId(), dsInfo.GetId(), dsInfo.GetName(), dsInfoHash)) } - ds := b.newZabbixDatasource() - b.datasourceCache.Set(dsInfoHash, ds) - return ds + ds, err := p.NewZabbixDatasource(dsInfo) + if err != nil { + return nil, err + } + + p.datasourceCache.Set(dsInfoHash, ds) + return ds, nil } // GetQueryType determines the query type from a query or list of queries diff --git a/pkg/datasource_test.go b/pkg/datasource_test.go index 2d861fe..9ac424a 100644 --- a/pkg/datasource_test.go +++ b/pkg/datasource_test.go @@ -15,18 +15,21 @@ func TestZabbixBackend_getCachedDatasource(t *testing.T) { basicDatasourceInfo := &datasource.DatasourceInfo{ Id: 1, Name: "TestDatasource", + Url: "http://zabbix.org/zabbix", } basicDatasourceInfoHash := HashDatasourceInfo(basicDatasourceInfo) - modifiedDatasource := NewZabbixDatasource() + modifiedDatasource, _ := newZabbixDatasource(basicDatasourceInfo) modifiedDatasource.authToken = "AB404F1234" altDatasourceInfo := &datasource.DatasourceInfo{ Id: 2, Name: "AnotherDatasource", + Url: "http://zabbix.org/another/zabbix", } altDatasourceInfoHash := HashDatasourceInfo(altDatasourceInfo) + basicDS, _ := newZabbixDatasource(basicDatasourceInfo) tests := []struct { name string cache *cache.Cache @@ -38,7 +41,7 @@ func TestZabbixBackend_getCachedDatasource(t *testing.T) { request: &datasource.DatasourceRequest{ Datasource: basicDatasourceInfo, }, - want: NewZabbixDatasource(), + want: basicDS, }, { name: "Uncached Datasource (cache miss)", @@ -48,12 +51,12 @@ func TestZabbixBackend_getCachedDatasource(t *testing.T) { request: &datasource.DatasourceRequest{ Datasource: altDatasourceInfo, }, - want: NewZabbixDatasource(), + want: basicDS, }, { name: "Cached Datasource", cache: cache.NewFrom(cache.NoExpiration, cache.NoExpiration, map[string]cache.Item{ - altDatasourceInfoHash: cache.Item{Object: NewZabbixDatasource()}, + altDatasourceInfoHash: cache.Item{Object: basicDS}, basicDatasourceInfoHash: cache.Item{Object: modifiedDatasource}, }), request: &datasource.DatasourceRequest{ @@ -67,14 +70,14 @@ func TestZabbixBackend_getCachedDatasource(t *testing.T) { if tt.cache == nil { tt.cache = cache.New(cache.NoExpiration, cache.NoExpiration) } - b := &ZabbixBackend{ + b := &ZabbixPlugin{ logger: hclog.New(&hclog.LoggerOptions{ Name: "TestZabbixBackend_getCachedDatasource", Level: hclog.LevelFromString("DEBUG"), }), datasourceCache: &Cache{cache: tt.cache}, } - got := b.getCachedDatasource(tt.request) + got, _ := b.GetDatasource(tt.request) // Only checking the authToken, being the easiest value to, and guarantee equality for assert.Equal(t, tt.want.authToken, got.authToken) diff --git a/pkg/models.go b/pkg/models.go index be0b258..a304d12 100644 --- a/pkg/models.go +++ b/pkg/models.go @@ -20,8 +20,8 @@ type requestModel struct { } type queryRequest struct { - Method string `json:"method,omitempty"` - Params zabbixParams `json:"params,omitempty"` + Method string `json:"method,omitempty"` + Params ZabbixAPIParams `json:"params,omitempty"` } type zabbixParamOutput struct { @@ -60,7 +60,7 @@ func (p *zabbixParamOutput) UnmarshalJSON(data []byte) error { } -type zabbixParams struct { +type ZabbixAPIParams struct { Output *zabbixParamOutput `json:"output,omitempty"` SortField string `json:"sortfield,omitempty"` SortOrder string `json:"sortorder,omitempty"` diff --git a/pkg/models_test.go b/pkg/models_test.go index 22c6128..b59c992 100644 --- a/pkg/models_test.go +++ b/pkg/models_test.go @@ -11,12 +11,12 @@ import ( func Test_zabbixParamOutput(t *testing.T) { tests := []struct { name string - input zabbixParams + input ZabbixAPIParams want string }{ { name: "Mode extend", - input: zabbixParams{ + input: ZabbixAPIParams{ Output: &zabbixParamOutput{ Mode: "extend", }, @@ -26,7 +26,7 @@ func Test_zabbixParamOutput(t *testing.T) { }, { name: "Fields", - input: zabbixParams{ + input: ZabbixAPIParams{ Output: &zabbixParamOutput{ Fields: []string{"name", "key_", "hostid"}, }, @@ -36,7 +36,7 @@ func Test_zabbixParamOutput(t *testing.T) { }, { name: "No Output", - input: zabbixParams{ + input: ZabbixAPIParams{ GroupIDs: []string{"test1", "test2"}, }, want: `{ "groupids": ["test1", "test2"] }`, @@ -51,7 +51,7 @@ func Test_zabbixParamOutput(t *testing.T) { return } - objOut := zabbixParams{} + objOut := ZabbixAPIParams{} err = json.Unmarshal(jsonOut, &objOut) assert.NoError(t, err) assert.Equal(t, tt.input, objOut) diff --git a/pkg/plugin.go b/pkg/plugin.go index 33b5442..ac42670 100644 --- a/pkg/plugin.go +++ b/pkg/plugin.go @@ -24,7 +24,7 @@ func main() { MagicCookieValue: "datasource", }, Plugins: map[string]plugin.Plugin{ - "zabbix-backend-datasource": &datasource.DatasourcePluginImpl{Plugin: &ZabbixBackend{ + "zabbix-backend-datasource": &datasource.DatasourcePluginImpl{Plugin: &ZabbixPlugin{ datasourceCache: NewCache(10*time.Minute, 10*time.Minute), logger: pluginLogger, }}, diff --git a/pkg/zabbix_api.go b/pkg/zabbix_api.go index 2facf3e..c847f5b 100644 --- a/pkg/zabbix_api.go +++ b/pkg/zabbix_api.go @@ -1,38 +1,20 @@ package main import ( - "bytes" - "crypto/tls" "encoding/json" "errors" "fmt" - "io" - "io/ioutil" "math" - "net" - "net/http" - "net/url" "regexp" "time" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" simplejson "github.com/bitly/go-simplejson" "github.com/grafana/grafana_plugin_model/go/datasource" - hclog "github.com/hashicorp/go-hclog" "golang.org/x/net/context" - "golang.org/x/net/context/ctxhttp" ) -// ZabbixDatasource stores state about a specific datasource and provides methods to make -// requests to the Zabbix API -type ZabbixDatasource struct { - queryCache *Cache - logger hclog.Logger - httpClient *http.Client - authToken string -} - -type categories struct { +type FunctionCategories struct { Transform []map[string]interface{} Aggregate []map[string]interface{} Filter []map[string]interface{} @@ -42,37 +24,11 @@ type categories struct { Special []map[string]interface{} } -// NewZabbixDatasource returns an initialized ZabbixDatasource -func NewZabbixDatasource() *ZabbixDatasource { - return &ZabbixDatasource{ - queryCache: NewCache(10*time.Minute, 10*time.Minute), - httpClient: &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), - }, - } -} - // ZabbixAPIQuery handles query requests to Zabbix func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { result, queryExistInCache := ds.queryCache.Get(HashString(tsdbReq.String())) if !queryExistInCache { - dsInfo := tsdbReq.GetDatasource() - queries := []requestModel{} for _, query := range tsdbReq.Queries { req := requestModel{} @@ -90,7 +46,7 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou query := queries[0].Target var err error - result, err = ds.ZabbixRequest(ctx, dsInfo, query.Method, query.Params) + result, err = ds.ZabbixRequest(ctx, query.Method, query.Params) ds.queryCache.Set(HashString(tsdbReq.String()), result) if err != nil { ds.logger.Debug("ZabbixAPIQuery", "error", err) @@ -103,15 +59,12 @@ func (ds *ZabbixDatasource) ZabbixAPIQuery(ctx context.Context, tsdbReq *datasou // TestConnection checks authentication and version of the Zabbix API and returns that info func (ds *ZabbixDatasource) TestConnection(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { - dsInfo := tsdbReq.GetDatasource() - - auth, err := ds.loginWithDs(ctx, dsInfo) + err := ds.loginWithDs(ctx) if err != nil { return BuildErrorResponse(fmt.Errorf("Authentication failed: %s", err)), nil } - ds.authToken = auth - response, err := ds.zabbixAPIRequest(ctx, dsInfo.GetUrl(), "apiinfo.version", zabbixParams{}, "") + response, err := ds.ZabbixAPIRequest(ctx, "apiinfo.version", ZabbixAPIParams{}, "") if err != nil { ds.logger.Debug("TestConnection", "error", err) return BuildErrorResponse(fmt.Errorf("Version check failed: %s", err)), nil @@ -127,160 +80,6 @@ func (ds *ZabbixDatasource) TestConnection(ctx context.Context, tsdbReq *datasou return BuildResponse(testResponse) } -// ZabbixRequest checks authentication and makes a request to the Zabbix API -func (ds *ZabbixDatasource) ZabbixRequest(ctx context.Context, dsInfo *datasource.DatasourceInfo, method string, params zabbixParams) (*simplejson.Json, error) { - zabbixURL := dsInfo.GetUrl() - - var result *simplejson.Json - var err error - - for attempt := 0; attempt <= 3; attempt++ { - if ds.authToken == "" { - // Authenticate - ds.authToken, err = ds.loginWithDs(ctx, dsInfo) - if err != nil { - return nil, err - } - } - result, err = ds.zabbixAPIRequest(ctx, zabbixURL, method, params, ds.authToken) - if err == nil || (err != nil && !isNotAuthorized(err.Error())) { - break - } else { - ds.authToken = "" - } - } - return result, err -} - -func (ds *ZabbixDatasource) loginWithDs(ctx context.Context, dsInfo *datasource.DatasourceInfo) (string, error) { - zabbixURLStr := dsInfo.GetUrl() - zabbixURL, err := url.Parse(zabbixURLStr) - if err != nil { - return "", err - } - - jsonDataStr := dsInfo.GetJsonData() - jsonData, err := simplejson.NewJson([]byte(jsonDataStr)) - if err != nil { - return "", err - } - - zabbixLogin := jsonData.Get("username").MustString() - var zabbixPassword string - if securePassword, exists := dsInfo.GetDecryptedSecureJsonData()["password"]; exists { - zabbixPassword = securePassword - } else { - zabbixPassword = jsonData.Get("password").MustString() - } - - auth, err := ds.login(ctx, zabbixURLStr, zabbixLogin, zabbixPassword) - if err != nil { - ds.logger.Error("loginWithDs", "error", err) - return "", err - } - ds.logger.Debug("loginWithDs", "url", zabbixURL, "user", zabbixLogin, "auth", auth) - - return auth, nil -} - -func (ds *ZabbixDatasource) login(ctx context.Context, apiURL string, username string, password string) (string, error) { - params := zabbixParams{ - User: username, - Password: password, - } - auth, err := ds.zabbixAPIRequest(ctx, apiURL, "user.login", params, "") - if err != nil { - return "", err - } - - return auth.MustString(), nil -} - -func (ds *ZabbixDatasource) zabbixAPIRequest(ctx context.Context, apiURL string, method string, params zabbixParams, auth string) (*simplejson.Json, error) { - zabbixURL, err := url.Parse(apiURL) - - // TODO: inject auth token (obtain from 'user.login' first) - 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: zabbixURL, - Header: map[string][]string{ - "Content-Type": {"application/json"}, - }, - Body: rc, - } - - tStart := time.Now() - response, err := makeHTTPRequest(ctx, ds.httpClient, req) - if err != nil { - return nil, err - } - - requestTime := time.Now().Sub(tStart) - ds.logger.Debug("Response from Zabbix Request", "method", method, "requestTime", requestTime) - - 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." -} - func (ds *ZabbixDatasource) queryNumericItems(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { tStart := time.Now() jsonQueries := make([]*simplejson.Json, 0) @@ -490,7 +289,7 @@ func (ds *ZabbixDatasource) getGroups(ctx context.Context, dsInfo *datasource.Da } func (ds *ZabbixDatasource) getAllItems(ctx context.Context, dsInfo *datasource.DatasourceInfo, hostids []string, appids []string, itemtype string) (*simplejson.Json, error) { - params := zabbixParams{ + params := ZabbixAPIParams{ Output: &zabbixParamOutput{Fields: []string{"itemid", "name", "key_", "value_type", "hostid", "status", "state"}}, SortField: "name", WebItems: true, @@ -506,25 +305,25 @@ func (ds *ZabbixDatasource) getAllItems(ctx context.Context, dsInfo *datasource. params.Filter["value_type"] = []int{1, 2, 4} } - return ds.ZabbixRequest(ctx, dsInfo, "item.get", params) + return ds.ZabbixRequest(ctx, "item.get", params) } func (ds *ZabbixDatasource) getAllApps(ctx context.Context, dsInfo *datasource.DatasourceInfo, hostids []string) (*simplejson.Json, error) { - params := zabbixParams{Output: &zabbixParamOutput{Mode: "extend"}, HostIDs: hostids} + params := ZabbixAPIParams{Output: &zabbixParamOutput{Mode: "extend"}, HostIDs: hostids} - return ds.ZabbixRequest(ctx, dsInfo, "application.get", params) + return ds.ZabbixRequest(ctx, "application.get", params) } func (ds *ZabbixDatasource) getAllHosts(ctx context.Context, dsInfo *datasource.DatasourceInfo, groupids []string) (*simplejson.Json, error) { - params := zabbixParams{Output: &zabbixParamOutput{Fields: []string{"name", "host"}}, SortField: "name", GroupIDs: groupids} + params := ZabbixAPIParams{Output: &zabbixParamOutput{Fields: []string{"name", "host"}}, SortField: "name", GroupIDs: groupids} - return ds.ZabbixRequest(ctx, dsInfo, "host.get", params) + return ds.ZabbixRequest(ctx, "host.get", params) } func (ds *ZabbixDatasource) getAllGroups(ctx context.Context, dsInfo *datasource.DatasourceInfo) (*simplejson.Json, error) { - params := zabbixParams{Output: &zabbixParamOutput{Fields: []string{"name"}}, SortField: "name", RealHosts: true} + params := ZabbixAPIParams{Output: &zabbixParamOutput{Fields: []string{"name"}}, SortField: "name", RealHosts: true} - return ds.ZabbixRequest(ctx, dsInfo, "hostgroup.get", params) + return ds.ZabbixRequest(ctx, "hostgroup.get", params) } func (ds *ZabbixDatasource) queryNumericDataForItems(ctx context.Context, tsdbReq *datasource.DatasourceRequest, items zabbix.Items, jsonQueries []*simplejson.Json, useTrend bool) ([]*datasource.TimeSeries, error) { @@ -548,7 +347,7 @@ func (ds *ZabbixDatasource) getTrendValueType(jsonQueries []*simplejson.Json) st var trendValueFunc string // TODO: loop over populated categories - for _, j := range new(categories).Trends { + for _, j := range new(FunctionCategories).Trends { trendFunctions = append(trendFunctions, j["name"].(string)) } for _, k := range jsonQueries[0].Get("functions").MustArray() { @@ -596,7 +395,7 @@ func (ds *ZabbixDatasource) getHistotyOrTrend(ctx context.Context, tsdbReq *data itemids = append(itemids, m.ID) } - params := zabbixParams{ + params := ZabbixAPIParams{ Output: &zabbixParamOutput{Mode: "extend"}, SortField: "clock", SortOrder: "ASC", @@ -608,10 +407,10 @@ func (ds *ZabbixDatasource) getHistotyOrTrend(ctx context.Context, tsdbReq *data var response *simplejson.Json var err error if useTrend { - response, err = ds.ZabbixRequest(ctx, tsdbReq.GetDatasource(), "trend.get", params) + response, err = ds.ZabbixRequest(ctx, "trend.get", params) } else { params.History = &k - response, err = ds.ZabbixRequest(ctx, tsdbReq.GetDatasource(), "history.get", params) + response, err = ds.ZabbixRequest(ctx, "history.get", params) } if err != nil { diff --git a/pkg/zabbix_api_core.go b/pkg/zabbix_api_core.go new file mode 100644 index 0000000..c1c62f5 --- /dev/null +++ b/pkg/zabbix_api_core.go @@ -0,0 +1,209 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "time" + + simplejson "github.com/bitly/go-simplejson" + "github.com/grafana/grafana_plugin_model/go/datasource" + hclog "github.com/hashicorp/go-hclog" + "golang.org/x/net/context/ctxhttp" +) + +// ZabbixDatasource stores state about a specific datasource and provides methods to make +// requests to the Zabbix API +type ZabbixDatasource struct { + url *url.URL + authToken string + dsInfo *datasource.DatasourceInfo + queryCache *Cache + logger hclog.Logger + httpClient *http.Client +} + +// newZabbixDatasource returns an initialized ZabbixDatasource +func newZabbixDatasource(dsInfo *datasource.DatasourceInfo) (*ZabbixDatasource, error) { + zabbixURLStr := dsInfo.GetUrl() + zabbixURL, err := url.Parse(zabbixURLStr) + if err != nil { + return nil, err + } + + return &ZabbixDatasource{ + url: zabbixURL, + dsInfo: dsInfo, + queryCache: NewCache(10*time.Minute, 10*time.Minute), + httpClient: &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), + }, + }, nil +} + +// ZabbixRequest checks authentication and makes a request to the Zabbix API +func (ds *ZabbixDatasource) ZabbixRequest(ctx context.Context, method string, params ZabbixAPIParams) (*simplejson.Json, error) { + var result *simplejson.Json + var err error + + 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 *ZabbixDatasource) loginWithDs(ctx context.Context) error { + jsonDataStr := ds.dsInfo.GetJsonData() + jsonData, err := simplejson.NewJson([]byte(jsonDataStr)) + if err != nil { + return err + } + + zabbixLogin := jsonData.Get("username").MustString() + var zabbixPassword string + if securePassword, exists := ds.dsInfo.GetDecryptedSecureJsonData()["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 *ZabbixDatasource) 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 *ZabbixDatasource) 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, + } + + tStart := time.Now() + response, err := makeHTTPRequest(ctx, ds.httpClient, req) + if err != nil { + return nil, err + } + + requestTime := time.Now().Sub(tStart) + ds.logger.Debug("Response from Zabbix Request", "method", method, "requestTime", requestTime) + + 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_api_test.go index 0eaee01..3e40f40 100644 --- a/pkg/zabbix_api_test.go +++ b/pkg/zabbix_api_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "net/http" + "net/url" "regexp" "testing" "time" @@ -31,7 +32,7 @@ func NewTestClient(fn RoundTripFunc) *http.Client { var basicDatasourceInfo = &datasource.DatasourceInfo{ Id: 1, Name: "TestDatasource", - Url: "sameUrl", + Url: "http://zabbix.org/zabbix", JsonData: `{"username":"username", "password":"password"}}`, } @@ -47,7 +48,10 @@ func mockDataSourceRequest(modelJSON string) *datasource.DatasourceRequest { } func mockZabbixDataSource(body string, statusCode int) ZabbixDatasource { + apiUrl, _ := url.Parse(basicDatasourceInfo.Url) return ZabbixDatasource{ + url: apiUrl, + dsInfo: basicDatasourceInfo, queryCache: NewCache(10*time.Minute, 10*time.Minute), httpClient: NewTestClient(func(req *http.Request) *http.Response { return &http.Response{ @@ -102,7 +106,7 @@ func TestZabbixAPIQueryError(t *testing.T) { func TestLogin(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) - resp, err := zabbixDatasource.login(context.Background(), "apiURL", "username", "password") + resp, err := zabbixDatasource.login(context.Background(), "username", "password") assert.Equal(t, "sampleResult", resp) assert.Nil(t, err) @@ -110,7 +114,7 @@ func TestLogin(t *testing.T) { func TestLoginError(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) - resp, err := zabbixDatasource.login(context.Background(), "apiURL", "username", "password") + resp, err := zabbixDatasource.login(context.Background(), "username", "password") assert.Equal(t, "", resp) assert.NotNil(t, err) @@ -118,55 +122,45 @@ func TestLoginError(t *testing.T) { func TestLoginWithDs(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) - resp, err := zabbixDatasource.loginWithDs(context.Background(), basicDatasourceInfo) + err := zabbixDatasource.loginWithDs(context.Background()) - assert.Equal(t, "sampleResult", resp) + assert.Equal(t, "sampleResult", zabbixDatasource.authToken) assert.Nil(t, err) } func TestLoginWithDsError(t *testing.T) { - zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) - resp, err := zabbixDatasource.loginWithDs(context.Background(), basicDatasourceInfo) + errResponse := `{"error":{"code":-32500,"message":"Application error.","data":"Login name or password is incorrect."}}` + zabbixDatasource := mockZabbixDataSource(errResponse, 200) + err := zabbixDatasource.loginWithDs(context.Background()) - assert.Equal(t, "", resp) + assert.Equal(t, "", zabbixDatasource.authToken) assert.NotNil(t, err) } func TestZabbixRequest(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) - resp, err := zabbixDatasource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + resp, err := zabbixDatasource.ZabbixRequest(context.Background(), "method", ZabbixAPIParams{}) assert.Equal(t, "sampleResult", resp.MustString()) assert.Nil(t, err) } func TestZabbixRequestWithNoAuthToken(t *testing.T) { - var mockDataSource = ZabbixDatasource{ - queryCache: NewCache(10*time.Minute, 10*time.Minute), - httpClient: NewTestClient(func(req *http.Request) *http.Response { - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(`{"result":"auth"}`)), - Header: make(http.Header), - } - }), - logger: hclog.Default(), - } - - resp, err := mockDataSource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + zabbixDatasource := mockZabbixDataSource(`{"result":"auth"}`, 200) + resp, err := zabbixDatasource.ZabbixRequest(context.Background(), "method", ZabbixAPIParams{}) assert.Equal(t, "auth", resp.MustString()) assert.Nil(t, err) } func TestZabbixRequestError(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) - resp, err := zabbixDatasource.ZabbixRequest(context.Background(), basicDatasourceInfo, "method", zabbixParams{}) + resp, err := zabbixDatasource.ZabbixRequest(context.Background(), "method", ZabbixAPIParams{}) assert.Nil(t, resp) assert.NotNil(t, err) } func TestZabbixAPIRequest(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 200) - resp, err := zabbixDatasource.zabbixAPIRequest(context.Background(), "apiURL", "item.get", zabbixParams{}, "auth") + resp, err := zabbixDatasource.ZabbixAPIRequest(context.Background(), "item.get", ZabbixAPIParams{}, "auth") assert.Equal(t, "sampleResult", resp.MustString()) assert.Nil(t, err) @@ -174,7 +168,7 @@ func TestZabbixAPIRequest(t *testing.T) { func TestZabbixAPIRequestError(t *testing.T) { zabbixDatasource := mockZabbixDataSource(`{"result":"sampleResult"}`, 500) - resp, err := zabbixDatasource.zabbixAPIRequest(context.Background(), "apiURL", "item.get", zabbixParams{}, "auth") + resp, err := zabbixDatasource.ZabbixAPIRequest(context.Background(), "item.get", ZabbixAPIParams{}, "auth") assert.Nil(t, resp) assert.NotNil(t, err)