Commit Diff


commit - 6e415953de07ac11e0f54ac397d156b23f384a15
commit + 5055878019b72a9ed3408e337a21e868c3842cf5
blob - 89a3a5d5f479418d18c6e8b14503b9e41263a13a
blob + bdf22d2e28488f143a1bac4f1483ab50641bc261
--- host.go
+++ host.go
@@ -1,20 +1,78 @@
 package icinga
 
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+// Host represents a Host object.
 type Host struct {
-	Name  string
-	State int
+	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"`
 }
 
+type hostresults struct {
+	Results []hostresult `json:"results"`
+	results
+}
+
+type hostresult struct {
+	Host Host `json:"attrs"`
+	result
+}
+
 var ErrNoHost = errors.New("no such host")
 
+func (h Host) MarshalJSON() ([]byte, error) {
+	type Attrs struct {
+		Address      string `json:"address"`
+		CheckCommand string `json:"check_command"`
+		DisplayName  string `json:"display_name"`
+	}
+	type host struct {
+		Attrs Attrs `json:"attrs"`
+	}
+	jhost := &host{
+		Attrs: Attrs{
+			Address:      h.Address,
+			CheckCommand: h.CheckCommand,
+			DisplayName:  h.DisplayName,
+		},
+	}
+	return json.Marshal(jhost)
+}
+
+// Hosts returns all Hosts in the Icinga2 configuration.
 func (c *Client) Hosts() ([]Host, error) {
-	_, err := c.get("/objects/hosts")
+	resp, err := c.get("/objects/hosts")
 	if err != nil {
 		return nil, err
 	}
-	return nil, err
+	defer resp.Body.Close()
+	var res hostresults
+	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
+		return nil, err
+	}
+	if res.Err() != nil {
+		return nil, res.Err()
+	}
+	var hosts []Host
+	for _, r := range res.Results {
+		hosts = append(hosts, r.Host)
+	}
+	return hosts, nil
 }
 
+// LookupHost returns the Host identified by name.
+// If no Host is found, error wraps ErrNoHost.
 func (c *Client) LookupHost(name string) (Host, error) {
 	resp, err := c.get("/objects/hosts/" + name)
 	if err != nil {
@@ -26,6 +84,21 @@ func (c *Client) LookupHost(name string) (Host, error)
 	return Host{}, err
 }
 
+// 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 {
+		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.
 func (c *Client) DeleteHost(name string) error {
 	if err := c.delete("/objects/hosts/" + name); err != nil {
 		return fmt.Errorf("delete host %s: %w", name, err)
blob - 288e822520bdd2fc700aaa803244e7e0b422362a
blob + f7e037875aad09f3f9c8d9d471b89047f21183d9
--- http.go
+++ http.go
@@ -1,6 +1,8 @@
 package icinga
 
 import (
+	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -14,7 +16,7 @@ type results struct {
 }
 
 type result struct {
-	Attrs  interface{}
+	Attrs  map[string]interface{}
 	Code   int
 	Errors []string
 	Name   string
@@ -23,18 +25,23 @@ type result struct {
 
 var ErrNoObject = errors.New("no such object")
 
-func (res results) Error() string {
-	var s []string
+func (res results) Err() error {
+	if len(res.Results) == 0 {
+		return nil
+	}
+	var errs []string
 	for _, r := range res.Results {
-		s = append(s, r.Error())
+		if len(r.Errors) == 0 {
+			continue
+		}
+		errs = append(errs, strings.Join(r.Errors, ", "))
 	}
-	return strings.Join(s, ", ")
+	if len(errs) == 0 {
+		return nil
+	}
+	return errors.New(strings.Join(errs, ", "))
 }
 
-func (r result) Error() string {
-	return strings.Join(r.Errors, ", ")
-}
-
 func newRequest(method, host, path string, body io.Reader) (*http.Request, error) {
 	url := "https://" + host + versionPrefix + path
 	req, err := http.NewRequest(method, url, body)
@@ -71,18 +78,30 @@ func (c *Client) post(path string, body io.Reader) (*h
 	return c.do(req)
 }
 
-func (c *Client) put(path string, body io.Reader) (*http.Response, error) {
+func (c *Client) put(path string, body io.Reader) error {
 	req, err := newRequest(http.MethodPut, c.host, path, body)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	return c.do(req)
+	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()
 }
 
 func (c *Client) delete(path string) error {
 	req, err := newRequest(http.MethodDelete, c.host, path, nil)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	resp, err := c.do(req)
 	if err != nil {
@@ -98,7 +117,7 @@ func (c *Client) delete(path string) error {
 	if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
 		return fmt.Errorf("decode response: %w", err)
 	}
-	return results
+	return results.Err()
 }
 
 func (c *Client) do(req *http.Request) (*http.Response, error) {
blob - 75cbc2e9e9ee0b2dc54816cbccab615657ab52a7
blob + a268dee69097cbccbda0932108fc00149c0685ef
--- icinga.go
+++ icinga.go
@@ -1,3 +1,26 @@
+// package icinga provides a client to the Icinga2 HTTP API.
+//
+// A Client manages interaction with an Icinga2 server.
+// It is created using Dial:
+//
+//	client, err := icinga.Dial("icinga.example.com:5665", "icinga", "secret", http.DefaultClient)
+//	if err != nil {
+//		// handle error
+//	}
+//	host, err := icinga.LookupHost("myserver.example.com")
+//	if err != nil {
+//		// handle error
+//	}
+//
+// Since Client wraps http.Client, exported methods of http.Client such
+// as Get and PostForm can be used to implement any extra functionality
+// not provided by this package. For example:
+//
+//	resp, err := client.PostForm("https://icinga.example.com:5665", data)
+//	if err != nil {
+//		// handle error
+//	}
+//
 package icinga
 
 import (
@@ -5,15 +28,24 @@ import (
 	"net/http"
 )
 
+// 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
+// PostForm can be used to implement any functionality not provided by
+// methods of Client.
 type Client struct {
-	host     string
+	addr     string
 	username string
 	password string
 	*http.Client
 }
 
-func Dial(host, username, password string, client *http.Client) (*Client, error) {
-	c := &Client{host, username, password, client}
+// 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 {
 		return nil, err
 	}
blob - 64068f9309c6e8d5c6a9010a7480b5947d86d288
blob + ed0753ef3181ebb0e5612b6d812b2bc7fc1d7441
--- user.go
+++ user.go
@@ -8,6 +8,8 @@ import (
 	"net/http"
 )
 
+// User represents a User object.
+// Note that this is different from an ApiUser.
 type User struct {
 	Name   string
 	Email  string
@@ -47,26 +49,21 @@ func (c *Client) LookupUser(name string) (User, error)
 	return testUser, nil
 }
 
+// CreateUser creates the User u identified by u.Name.
+// An error is returned if the User already exists or on any other error.
 func (c *Client) CreateUser(u User) error {
 	buf := &bytes.Buffer{}
 	if err := json.NewEncoder(buf).Encode(u); err != nil {
 		return err
 	}
-	resp, err := c.put("/objects/users/"+u.Name, buf)
-	if err != nil {
+	if err := c.put("/objects/users/"+u.Name, buf); err != nil {
 		return fmt.Errorf("create %s: %w", u.Name, 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("create %s: decode response: %w", u.Name, err)
-	}
-	return fmt.Errorf("create %s: %w", u.Name, results)
+	return nil
 }
 
+// DeleteUser deletes the User identified by name.
+// ErrNoUser is returned if the User doesn't exist.
 func (c *Client) DeleteUser(name string) error {
 	if err := c.delete("/objects/users/" + name); err != nil {
 		return fmt.Errorf("delete user %s: %w", name, err)