commit - 514f2cd100380e2d124fa33c56f4e9a9308b907e
commit + 76669f9b1f06b7299f2261667b4b21db38db7cae
blob - bdf22d2e28488f143a1bac4f1483ab50641bc261
blob + 6a3c6313b97f01ce8acaec5a60fe3dda545aaa67
--- host.go
+++ host.go
package icinga
import (
- "bytes"
"encoding/json"
- "errors"
"fmt"
- "net/http"
)
// Host represents a Host object.
type Host struct {
- Name string `json:"name"`
- Address string `json:"address"`
- Address6 string `json:"address6"`
- Groups []string `json:"groups"`
- State int `json:"state"`
- CheckCommand string `json:"check_command"`
- DisplayName string `json:"display_name"`
+ Name string `json:"name"`
+ Address string `json:"address"`
+ Address6 string `json:"address6"`
+ Groups []string `json:"groups"`
+ State HostState `json:"state"`
+ CheckCommand string `json:"check_command"`
+ DisplayName string `json:"display_name"`
}
-type hostresults struct {
- Results []hostresult `json:"results"`
- results
+type HostState int
+
+const (
+ HostUp HostState = 0 + iota
+ HostDown
+ HostUnreachable
+)
+
+func (s HostState) String() string {
+ switch s {
+ case HostUp:
+ return "HostUp"
+ case HostDown:
+ return "HostDown"
+ case HostUnreachable:
+ return "HostUnreachable"
+ }
+ return "unhandled host state"
}
-type hostresult struct {
- Host Host `json:"attrs"`
- result
+func (h Host) name() string {
+ return h.Name
}
-var ErrNoHost = errors.New("no such host")
+func (h Host) path() string {
+ return "/objects/hosts/" + h.Name
+}
+func (h Host) attrs() map[string]interface{} {
+ m := make(map[string]interface{})
+ m["display_name"] = h.DisplayName
+ return m
+}
+
func (h Host) MarshalJSON() ([]byte, error) {
type Attrs struct {
Address string `json:"address"`
// Hosts returns all Hosts in the Icinga2 configuration.
func (c *Client) Hosts() ([]Host, error) {
- resp, err := c.get("/objects/hosts")
+ objects, err := c.allObjects("/objects/hosts")
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("get all hosts: %w", err)
}
- defer resp.Body.Close()
- var res hostresults
- if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
- return nil, err
+ var hosts []Host
+ for _, o := range objects {
+ v, ok := o.(Host)
+ if !ok {
+ return nil, fmt.Errorf("get all hosts: %T in response", v)
+ }
+ hosts = append(hosts, v)
}
- if res.Err() != nil {
- return nil, res.Err()
+ return hosts, nil
+}
+
+// FilterHosts returns any matching hosts after applying the filter
+// expression expr. If no hosts match, an empty slice and an error wrapping
+// ErrNoMatch is returned.
+func (c *Client) FilterHosts(expr string) ([]Host, error) {
+ objects, err := c.filterObjects("/objects/hosts", expr)
+ if err != nil {
+ return nil, fmt.Errorf("filter hosts %q: %w", expr, err)
}
var hosts []Host
- for _, r := range res.Results {
- hosts = append(hosts, r.Host)
+ for _, o := range objects {
+ v, ok := o.(Host)
+ if !ok {
+ return nil, fmt.Errorf("filter hosts %q: %T in response", expr, v)
+ }
+ hosts = append(hosts, v)
}
return hosts, nil
}
-// LookupHost returns the Host identified by name.
-// If no Host is found, error wraps ErrNoHost.
+// LookupHost returns the Host identified by name. If no Host is found,
+// error wraps ErrNotExist.
func (c *Client) LookupHost(name string) (Host, error) {
- resp, err := c.get("/objects/hosts/" + name)
+ obj, err := c.lookupObject("/objects/hosts/" + name)
if err != nil {
- return Host{}, err
+ return Host{}, fmt.Errorf("lookup %s: %w", name, err)
}
- if resp.StatusCode == http.StatusNotFound {
- return Host{}, fmt.Errorf("lookup %s: %w", name, ErrNoHost)
+ v, ok := obj.(Host)
+ if !ok {
+ return Host{}, fmt.Errorf("lookup %s: result type %T is not host", name, obj)
}
- return Host{}, err
+ return v, nil
}
// CreateHost creates the Host host.
// The Name and CheckCommand fields of host must be non-zero.
func (c *Client) CreateHost(host Host) error {
- buf := &bytes.Buffer{}
- if err := json.NewEncoder(buf).Encode(host); err != nil {
- return err
- }
- if err := c.put("/objects/hosts/"+host.Name, buf); err != nil {
+ if err := c.createObject(host); err != nil {
return fmt.Errorf("create host %s: %w", host.Name, err)
}
return nil
}
// DeleteHost deletes the Host identified by name.
-// If no Host is found, error wraps ErrNoObject.
+// If no Host is found, error wraps ErrNotExist.
func (c *Client) DeleteHost(name string) error {
- if err := c.delete("/objects/hosts/" + name); err != nil {
+ if err := c.deleteObject("/objects/hosts/" + name); err != nil {
return fmt.Errorf("delete host %s: %w", name, err)
}
return nil
blob - 3b165dbbfba9ae07b6bf6d4342b515dcc892d2ba
blob + 3707d01283233e4cd393c9b40e7c60481a44f64a
--- http.go
+++ http.go
package icinga
import (
- "encoding/json"
- "errors"
"fmt"
"io"
"net/http"
- "strings"
+ "net/url"
)
const versionPrefix = "/v1"
-type results struct {
- Results []result
-}
-
-type result struct {
- Attrs map[string]interface{}
- Code int
- Errors []string
- Name string
- Type string
-}
-
-var ErrNoObject = errors.New("no such object")
-
// NewRequest returns an authenticated HTTP request with appropriate header
// for sending to an Icinga2 server.
func NewRequest(method, url, username, password string, body io.Reader) (*http.Request, error) {
return nil, err
}
switch req.Method {
- case http.MethodGet:
- break
- case http.MethodDelete:
+ case http.MethodGet, http.MethodDelete:
req.Header.Set("Accept", "application/json")
case http.MethodPost, http.MethodPut:
req.Header.Set("Accept", "application/json")
return req, nil
}
-func (res results) Err() error {
- if len(res.Results) == 0 {
- return nil
- }
- var errs []string
- for _, r := range res.Results {
- if len(r.Errors) == 0 {
- continue
- }
- errs = append(errs, strings.Join(r.Errors, ", "))
- }
- if len(errs) == 0 {
- return nil
- }
- return errors.New(strings.Join(errs, ", "))
-}
-
func (c *Client) get(path string) (*http.Response, error) {
url := "https://" + c.addr + versionPrefix + path
req, err := NewRequest(http.MethodGet, url, c.username, c.password, nil)
return c.Do(req)
}
+func (c *Client) getFilter(path, filter string) (*http.Response, error) {
+ u, err := url.Parse("https://" + c.addr + versionPrefix + path)
+ if err != nil {
+ return nil, err
+ }
+ v := url.Values{}
+ v.Set("filter", filter)
+ u.RawQuery = v.Encode()
+ req, err := NewRequest(http.MethodGet, u.String(), c.username, c.password, nil)
+ if err != nil {
+ return nil, err
+ }
+ return c.Do(req)
+}
+
func (c *Client) post(path string, body io.Reader) (*http.Response, error) {
url := "https://" + c.addr + versionPrefix + path
req, err := NewRequest(http.MethodPost, url, c.username, c.password, body)
return c.Do(req)
}
-func (c *Client) put(path string, body io.Reader) error {
+func (c *Client) put(path string, body io.Reader) (*http.Response, error) {
url := "https://" + c.addr + versionPrefix + path
- req, err := NewRequest(http.MethodPost, url, c.username, c.password, body)
+ req, err := NewRequest(http.MethodPut, url, c.username, c.password, body)
if err != nil {
- return err
+ return nil, err
}
- resp, err := c.Do(req)
- if err != nil {
- return err
- }
- if resp.StatusCode == http.StatusOK {
- return nil
- }
- defer resp.Body.Close()
- var results results
- if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
- return fmt.Errorf("decode response: %w", err)
- }
- return results.Err()
+ return c.Do(req)
}
-func (c *Client) delete(path string) error {
+func (c *Client) delete(path string) (*http.Response, error) {
url := "https://" + c.addr + versionPrefix + path
- req, err := NewRequest(http.MethodPost, url, c.username, c.password, body)
+ req, err := NewRequest(http.MethodDelete, url, c.username, c.password, nil)
if err != nil {
- return err
+ return nil, err
}
- resp, err := c.Do(req)
- if err != nil {
- return err
- }
- if resp.StatusCode == http.StatusOK {
- return nil
- } else if resp.StatusCode == http.StatusNotFound {
- return ErrNoObject
- }
- defer resp.Body.Close()
- var results results
- if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
- return fmt.Errorf("decode response: %w", err)
- }
- return results.Err()
+ return c.Do(req)
}
blob - 2ccea26cf1583b0a427e2c2edda3753be3558ac8
blob + 48e63b7551f4a6a4cc152c7f1718598485dade49
--- icinga.go
+++ icinga.go
package icinga
import (
- "fmt"
+ "errors"
"net/http"
)
-type State int
-
-const (
- StateOK State = 0 + iota
- StateWarning
- StateCritical
- StateUnknown
-)
-
-const (
- HostUp State = 0 + iota
- HostDown
- HostUnknown
-)
-
// A Client represents a client connection to the Icinga2 HTTP API.
// It should be created using Dial.
// Since Client wraps http.Client, standard methods such as Get and
*http.Client
}
+var ErrNotExist = errors.New("object does not exist")
+var ErrExist = errors.New("object already exists")
+var ErrNoMatch = errors.New("no object matches filter")
+
// Dial returns a new Client connected to the Icinga2 server at addr.
// The recommended value for client is http.DefaultClient.
// But it may also be a modified client which, for example,
// skips TLS certificate verification.
func Dial(addr, username, password string, client *http.Client) (*Client, error) {
c := &Client{addr, username, password, client}
- if _, err := c.Status(); err != nil {
+ if _, err := c.Permissions(); err != nil {
return nil, err
}
return c, nil
}
-func (c *Client) Status() (*http.Response, error) {
- resp, err := c.get("/status")
+func (c *Client) Permissions() (response, error) {
+ resp, err := c.get("")
if err != nil {
- return nil, err
+ return response{}, err
}
- if resp.StatusCode != http.StatusOK {
- return resp, fmt.Errorf("status %s", resp.Status)
+ if resp.StatusCode == http.StatusOK {
+ return response{}, nil
}
- return resp, nil
+ return response{}, errors.New(resp.Status)
}
blob - 37e0a850e9dca14d633665a1a9b36fe32429d3cc
blob + 3a95a2a727cb7f5b20f7c920e4a44f4a2ba5fc19
--- user.go
+++ user.go
package icinga
import (
- "bytes"
"encoding/json"
- "errors"
"fmt"
- "net/http"
)
// User represents a User object.
Email: "test@example.com",
}
-var ErrNoUser = errors.New("no such user")
-
func (u User) MarshalJSON() ([]byte, error) {
- type Alias User
+ type attrs struct {
+ Email string `json:"email"`
+ Groups []string `json:"groups,omitempty"`
+ }
return json.Marshal(&struct {
- Attrs Alias
- }{Attrs: (Alias)(u)})
+ Attrs attrs `json:"attrs"`
+ }{
+ Attrs: attrs{
+ Email: u.Email,
+ Groups: u.Groups,
+ },
+ })
}
+func (u User) name() string {
+ return u.Name
+}
+
+func (u User) path() string {
+ return "/objects/users/" + u.Name
+}
+
+func (u User) attrs() map[string]interface{} {
+ m := make(map[string]interface{})
+ m["groups"] = u.Groups
+ m["email"] = u.Email
+ return m
+}
+
func (c *Client) Users() ([]User, error) {
- _, err := c.get("/objects/users")
+ objects, err := c.allObjects("/objects/users")
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("get all users: %w", err)
}
- return []User{testUser}, nil
+ var users []User
+ for _, o := range objects {
+ v, ok := o.(User)
+ if !ok {
+ return nil, fmt.Errorf("get all users: %T in response", v)
+ }
+ users = append(users, v)
+ }
+ return users, nil
}
func (c *Client) LookupUser(name string) (User, error) {
- resp, err := c.get("/objects/users/" + name)
+ obj, err := c.lookupObject("/objects/users/" + name)
if err != nil {
- return User{}, err
+ return User{}, fmt.Errorf("lookup %s: %w", name, err)
}
- if resp.StatusCode == http.StatusNotFound {
- return User{}, fmt.Errorf("lookup %s: %w", name, ErrNoUser)
+ v, ok := obj.(User)
+ if !ok {
+ return User{}, fmt.Errorf("lookup %s: result type %T is not user", name, v)
}
- return testUser, nil
+ return v, nil
}
// CreateUser creates user.
// An error is returned if the User already exists or on any other error.
func (c *Client) CreateUser(user User) error {
- buf := &bytes.Buffer{}
- if err := json.NewEncoder(buf).Encode(user); err != nil {
- return err
+ if err := c.createObject(user); err != nil {
+ return fmt.Errorf("create user %s: %w", user.Name, err)
}
- if err := c.put("/objects/users/"+user.Name, buf); err != nil {
- return fmt.Errorf("create %s: %w", user.Name, err)
- }
return nil
}
// DeleteUser deletes the User identified by name.
-// ErrNoUser is returned if the User doesn't exist.
+// ErrNotExist is returned if the User doesn't exist.
func (c *Client) DeleteUser(name string) error {
- if err := c.delete("/objects/users/" + name); err != nil {
+ if err := c.deleteObject("/objects/users/" + name); err != nil {
return fmt.Errorf("delete user %s: %w", name, err)
}
return nil
blob - /dev/null
blob + 4eee2ed00738ee81a70d73a95f2702910311639b (mode 644)
--- /dev/null
+++ object.go
+package icinga
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+type object interface {
+ name() string
+ attrs() map[string]interface{}
+ path() string
+}
+
+func (c *Client) lookupObject(objpath string) (object, error) {
+ resp, err := c.get(objpath)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrNotExist
+ }
+ iresp, err := parseResponse(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("parse response: %v", err)
+ } else if iresp.Error != nil {
+ return nil, iresp.Error
+ } else if resp.StatusCode != http.StatusOK {
+ return nil, errors.New(resp.Status)
+ }
+ return objectFromLookup(iresp)
+}
+
+func (c *Client) allObjects(objpath string) ([]object, error) {
+ resp, err := c.get(objpath)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ iresp, err := parseResponse(resp.Body)
+ if err != nil {
+ return nil, err
+ } else if iresp.Error != nil {
+ return nil, iresp.Error
+ } else if resp.StatusCode != http.StatusOK {
+ return nil, errors.New(resp.Status)
+ }
+ return iresp.Results, nil
+}
+
+func (c *Client) filterObjects(objpath, expr string) ([]object, error) {
+ resp, err := c.getFilter(objpath, expr)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrNoMatch
+ }
+ iresp, err := parseResponse(resp.Body)
+ if err != nil {
+ return nil, err
+ } else if iresp.Error != nil {
+ return nil, iresp.Error
+ } else if resp.StatusCode != http.StatusOK {
+ return nil, errors.New(resp.Status)
+ }
+ return iresp.Results, nil
+}
+
+func (c *Client) createObject(obj object) error {
+ buf := &bytes.Buffer{}
+ switch v := obj.(type) {
+ case Host:
+ if err := json.NewEncoder(buf).Encode(v); err != nil {
+ return err
+ }
+ case Service:
+ if err := json.NewEncoder(buf).Encode(v); err != nil {
+ return err
+ }
+ case User:
+ if err := json.NewEncoder(buf).Encode(v); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("create type %T unsupported", v)
+ }
+ resp, err := c.put(obj.path(), buf)
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ defer resp.Body.Close()
+ iresp, err := parseResponse(resp.Body)
+ if err != nil {
+ return fmt.Errorf("parse response: %v", err)
+ }
+ if strings.Contains(iresp.Error.Error(), "already exists") {
+ return ErrExist
+ }
+ return iresp.Error
+}
+
+func (c *Client) deleteObject(objpath string) error {
+ resp, err := c.delete(objpath)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ } else if resp.StatusCode == http.StatusNotFound {
+ return ErrNotExist
+ }
+ iresp, err := parseResponse(resp.Body)
+ if err != nil {
+ return fmt.Errorf("parse response: %v", err)
+ }
+ return iresp.Error
+}
blob - /dev/null
blob + faa8e3f3aaff33d56a12411ce4bb55b2269ac130 (mode 644)
--- /dev/null
+++ response.go
+package icinga
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+)
+
+type apiResponse struct {
+ Results []struct {
+ Name string
+ Type string
+ Errors []string
+ Attrs json.RawMessage
+ }
+ Status string
+}
+
+type response struct {
+ Results []object
+ Error error
+}
+
+func parseAPIResponse(r io.Reader) (apiResponse, error) {
+ var apiresp apiResponse
+ if err := json.NewDecoder(r).Decode(&apiresp); err != nil {
+ return apiResponse{}, err
+ }
+ return apiresp, nil
+}
+
+func parseResponse(r io.Reader) (*response, error) {
+ apiresp, err := parseAPIResponse(r)
+ if err != nil {
+ return nil, err
+ }
+ // Confusingly the top-level status field in an API response contains
+ // an error message. Successful statuses are actually held in the
+ // status field in Results!
+ if apiresp.Status != "" {
+ return &response{Error: errors.New(apiresp.Status)}, nil
+ }
+ resp := &response{}
+ for _, r := range apiresp.Results {
+ if len(r.Errors) > 0 {
+ resp.Error = errors.New(strings.Join(r.Errors, ", "))
+ // got an error so nothing left in the API response
+ break
+ }
+ if r.Type == "" {
+ continue //
+ }
+ switch r.Type {
+ case "Host":
+ var h Host
+ if err := json.Unmarshal(r.Attrs, &h); err != nil {
+ return nil, err
+ }
+ resp.Results = append(resp.Results, h)
+ case "Service":
+ var s Service
+ if err := json.Unmarshal(r.Attrs, &s); err != nil {
+ return nil, err
+ }
+ resp.Results = append(resp.Results, s)
+ case "User":
+ var u User
+ if err := json.Unmarshal(r.Attrs, &u); err != nil {
+ return nil, err
+ }
+ resp.Results = append(resp.Results, u)
+ default:
+ return nil, fmt.Errorf("unsupported unmarshal of type %s", r.Type)
+ }
+ }
+ return resp, nil
+}
+
+func objectFromLookup(resp *response) (object, error) {
+ if len(resp.Results) == 0 {
+ return nil, errors.New("empty results")
+ } else if len(resp.Results) > 1 {
+ return nil, errors.New("too many results")
+ }
+ return resp.Results[0], nil
+}
blob - /dev/null
blob + 8d8a80f0fe3ffcf01b3a2054aaf32e0b6a8dccbf (mode 644)
--- /dev/null
+++ service.go
+package icinga
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+func (s Service) name() string {
+ return s.Name
+}
+
+func (s Service) path() string {
+ return "/objects/services/" + s.Name
+}
+
+func (s Service) attrs() map[string]interface{} {
+ m := make(map[string]interface{})
+ m["display_name"] = s.DisplayName
+ return m
+}
+
+// Service represents a Service object.
+type Service struct {
+ Name string `json:"__name"`
+ Groups []string
+ State ServiceState
+ CheckCommand string `json:"check_command"`
+ DisplayName string `json:"display_name:"`
+}
+
+type ServiceState int
+
+const (
+ ServiceOK ServiceState = 0 + iota
+ ServiceWarning
+ ServiceCritical
+ ServiceUnknown
+)
+
+func (s ServiceState) String() string {
+ switch s {
+ case ServiceOK:
+ return "ServiceOK"
+ case ServiceWarning:
+ return "ServiceWarning"
+ case ServiceCritical:
+ return "ServiceCritical"
+ case ServiceUnknown:
+ return "ServiceUnknown"
+ }
+ return "unhandled service state"
+}
+
+func (s Service) MarshalJSON() ([]byte, error) {
+ attrs := make(map[string]interface{})
+ if len(s.Groups) > 0 {
+ attrs["groups"] = s.Groups
+ }
+ attrs["check_command"] = s.CheckCommand
+ attrs["display_name"] = s.DisplayName
+ jservice := &struct {
+ Attrs map[string]interface{} `json:"attrs"`
+ }{Attrs: attrs}
+ return json.Marshal(jservice)
+}
+
+func (c *Client) CreateService(service Service) error {
+ if err := c.createObject(service); err != nil {
+ return fmt.Errorf("create service %s: %w", service.Name, err)
+ }
+ return nil
+}
+
+func (c *Client) LookupService(name string) (Service, error) {
+ obj, err := c.lookupObject("/objects/services/" + name)
+ if err != nil {
+ return Service{}, fmt.Errorf("lookup %s: %w", name, err)
+ }
+ v, ok := obj.(Service)
+ if !ok {
+ return Service{}, fmt.Errorf("lookup %s: result type %T is not service", name, obj)
+ }
+ return v, nil
+}