diff --git a/README.md b/README.md index ca8881b..f67936e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,72 @@ -# zabbix-go +zabbix +====== -This Go library implements the Zabbix 2.0 JRPC API - -Based on https://github.com/adubkov/zabbix \ No newline at end of file +This Go library implements the Zabbix 2.0 API. The Zabbix API is a JSONRPC +based API, although it is not compatable with Go's builtin JSONRPC libraries. +So we implement that JSONRPC, and provide data types that mimic Zabbbix's +return values. + +Based on https://github.com/adubkov/zabbix + +Connecting to the API +===================== + +```go +func main() { + api, err := zabbix.NewAPI("http://zabbix.yourhost.net/api_jsonrpc.php", "User", "Password") + if err != nil { + fmt.Println(err) + return + } + + versionresult, err := api.Version() + if err != nil { + fmt.Println(err) + } + + fmt.Println(versionresult) + + _, err = api.Login() + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Connected to API") +} +``` + +Making a call +============= + +I typically wrap the actual API call to hide the messy details. If the +response has an Error field, and the code is greater than 0, the API +will return that Error. Then my wrapper function return a ZabbixError +to the caller. + +```go +// Find and return a single host object by name +func GetHost(api *zabbix.API, host string) (zabbix.ZabbixHost, error) { + params := make(map[string]interface{}, 0) + filter := make(map[string]string, 0) + filter["host"] = host + params["filter"] = filter + params["output"] = "extend" + params["select_groups"] = "extend" + params["templated_hosts"] = 1 + ret, err := api.Host("get", params) + + // This happens if there was an RPC error + if err != nil { + return nil, err + } + + // If our call was successful + if len(ret) > 0 { + return ret[0], err + } + + // This will be the case if the RPC call was successful, but + // Zabbix had an issue with the data we passed. + return nil, &zabbix.ZabbixError{0,"","Host not found"} +} +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2212f65 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module zabbix + +go 1.16 diff --git a/zabbix.go b/zabbix.go new file mode 100644 index 0000000..349612a --- /dev/null +++ b/zabbix.go @@ -0,0 +1,287 @@ +package zabbix + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "time" +) + +/** +Zabbix and Go's RPC implementations don't play with each other.. at all. +So I've re-created the wheel at bit. +*/ +type JsonRPCResponse struct { + Jsonrpc string `json:"jsonrpc"` + Error ZabbixError `json:"error"` + Result interface{} `json:"result"` + Id int `json:"id"` +} + +type JsonRPCRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` + + // Zabbix 2.0: + // The "user.login" method must be called without the "auth" parameter + Auth string `json:"auth,omitempty"` + Id int `json:"id"` +} + +type ZabbixError struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data"` +} + +func (z *ZabbixError) Error() string { + return z.Data +} + +type ZabbixHost map[string]interface{} +type ZabbixGraph map[string]interface{} +type ZabbixGraphItem map[string]interface{} +type ZabbixHistoryItem struct { + Clock string `json:"clock"` + Value string `json:"value"` + Itemid string `json:"itemid"` +} + +type ZabbixTemplate map[string]interface{} +type ZabbixHostGroup map[string]interface{} + +type API struct { + url string + user string + passwd string + id int + auth string +} + +func NewAPI(server, user, passwd string) (*API, error) { + return &API{server, user, passwd, 0, ""}, nil +} + +func (api *API) GetAuth() string { + return api.auth +} + +/** +Each request establishes its own connection to the server. This makes it easy +to keep request/responses in order without doing any concurrency +*/ + +func (api *API) ZabbixRequest(method string, data interface{}) (JsonRPCResponse, error) { + // Setup our JSONRPC Request data + id := api.id + api.id = api.id + 1 + jsonobj := JsonRPCRequest{"2.0", method, data, api.auth, id} + encoded, err := json.Marshal(jsonobj) + + if err != nil { + return JsonRPCResponse{}, err + } + // Setup our HTTP request + client := &http.Client{ + Transport: &http.Transport{ + MaxIdleConnsPerHost: 10, + ResponseHeaderTimeout: 60 * time.Second, + DialContext: (&net.Dialer{Timeout: time.Second}).DialContext, + TLSClientConfig: &tls.Config{ + MaxVersion: tls.VersionTLS11, + InsecureSkipVerify: true, + }, + }, + } + request, err := http.NewRequest("POST", api.url, bytes.NewBuffer(encoded)) + if err != nil { + return JsonRPCResponse{}, err + } + request.Header.Add("Content-Type", "application/json-rpc") + if api.auth != "" { + // XXX Not required in practice, check spec + //request.SetBasicAuth(api.user, api.passwd) + //request.Header.Add("Authorization", api.auth) + } + + // Execute the request + response, err := client.Do(request) + if err != nil { + return JsonRPCResponse{}, err + } + + /** + We can't rely on response.ContentLength because it will + be set at -1 for large responses that are chunked. So + we treat each API response as streamed data. + */ + var result JsonRPCResponse + var buf bytes.Buffer + + _, err = io.Copy(&buf, response.Body) + if err != nil { + return JsonRPCResponse{}, err + } + + json.Unmarshal(buf.Bytes(), &result) + + response.Body.Close() + + return result, nil +} + +func (api *API) Login() (bool, error) { + params := make(map[string]string, 0) + params["user"] = api.user + params["password"] = api.passwd + + response, err := api.ZabbixRequest("user.login", params) + if err != nil { + fmt.Printf("Error: %s\n", err) + return false, err + } + + if response.Error.Code != 0 { + return false, &response.Error + } + + api.auth = response.Result.(string) + return true, nil +} + +func (api *API) Version() (string, error) { + response, err := api.ZabbixRequest("APIInfo.version", make(map[string]string, 0)) + if err != nil { + return "", err + } + + if response.Error.Code != 0 { + return "", &response.Error + } + + return response.Result.(string), nil +} + +/** +Interface to the user.* calls +*/ +func (api *API) User(method string, data interface{}) ([]interface{}, error) { + response, err := api.ZabbixRequest("user."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + return response.Result.([]interface{}), nil +} + +/** +Interface to the host.* calls +*/ +func (api *API) Host(method string, data interface{}) ([]ZabbixHost, error) { + response, err := api.ZabbixRequest("host."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + // XXX uhg... there has got to be a better way to convert the response + // to the type I want to return + res, err := json.Marshal(response.Result) + var ret []ZabbixHost + err = json.Unmarshal(res, &ret) + return ret, nil +} + +/** +Interface to the graph.* calls +*/ +func (api *API) Graph(method string, data interface{}) ([]ZabbixGraph, error) { + response, err := api.ZabbixRequest("graph."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + // XXX uhg... there has got to be a better way to convert the response + // to the type I want to return + res, err := json.Marshal(response.Result) + var ret []ZabbixGraph + err = json.Unmarshal(res, &ret) + return ret, nil +} + +/** +Interface to the history.* calls +*/ +func (api *API) History(method string, data interface{}) ([]ZabbixHistoryItem, error) { + response, err := api.ZabbixRequest("history."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + // XXX uhg... there has got to be a better way to convert the response + // to the type I want to return + res, err := json.Marshal(response.Result) + var ret []ZabbixHistoryItem + err = json.Unmarshal(res, &ret) + return ret, nil +} + +/** +Interface to the template.* calls +*/ +func (api *API) Template(method string, data interface{}) ([]ZabbixTemplate, error) { + response, err := api.ZabbixRequest("template."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + // XXX uhg... there has got to be a better way to convert the response + // to the type I want to return + res, err := json.Marshal(response.Result) + var ret []ZabbixTemplate + err = json.Unmarshal(res, &ret) + return ret, nil +} + +/** +Interface to the hostgroup.* calls +*/ +func (api *API) HostGroup(method string, data interface{}) ([]ZabbixHostGroup, error) { + response, err := api.ZabbixRequest("hostgroup."+method, data) + if err != nil { + return nil, err + } + + if response.Error.Code != 0 { + return nil, &response.Error + } + + res, err := json.Marshal(response.Result) + var ret []ZabbixHostGroup + err = json.Unmarshal(res, &ret) + return ret, nil +}