commit - 49e200e1bf5862fe2a396ba4b3fa1646aa09e3ee
commit + b717ce9bd51371aefd55ccd6ddf623f83aed8e29
blob - aee29003cd9406fed861d0fc0ecdff2202a5763d
blob + bf68dac0cd9179bf79782212e1b34e1c2207bcca
--- README.md
+++ README.md
### Tests
+Some tests use a fake, in-process Icinga server. Not all features of
+the API are implemented, but on any unsupported request it should
+report an error. The fake server uses an in-memory map to store
+Icinga2 objects, which maps object's path in the API request (e.g.
+"objects/hosts/text.example.com") to the object's attributes (e.g.
+`check_command` and `display_name`).
+
Some tests dial an instance of Icinga2 running on the loopback address
-and the standard Icinga2 port 5665 ("127.0.0.1:5665"). If this fails,
-those tests are skipped. To run these tests, create the following API
-user:
+and the standard Icinga2 port 5665 (`::1:5665`). If this fails, those
+tests are skipped. To run these tests, create the following API user:
- object ApiUser "root" {
- password = "icinga"
+ object ApiUser "icinga" {
+ password = name
permissions = [ "*" ]
}
-Getting data from 127.0.0.1:5665 to an Icinga server is left as an
-exercise to the reader!
+Getting data from the loopback interface to an Icinga server is left
+as an exercise to the reader!
Personally, I run an Alpine Linux virtual machine using qemu. You
could also use the [official Icinga2 container image][image].
blob - 35cc262e6e5be08aa939f6a08886d9d7c1f82d26
blob + 1f48b98148f9c7834c9e516cdfa9238b4c297c99
--- icinga_test.go
+++ icinga_test.go
import (
"crypto/tls"
+ "encoding/json"
"errors"
+ "fmt"
+ "io"
"math/rand"
"net/http"
+ "net/http/httptest"
+ "os"
+ "path"
"reflect"
"sort"
+ "strings"
"testing"
"time"
return string(b) + suffix
}
-func createTestHosts(c *icinga.Client) ([]icinga.Host, error) {
- hostgroup := icinga.HostGroup{Name: "test", DisplayName: "Test Group"}
- if err := c.CreateHostGroup(hostgroup); err != nil && !errors.Is(err, icinga.ErrExist) {
- return nil, err
- }
+func randomTestAddr() string { return fmt.Sprintf("192.0.2.%d", rand.Intn(254)) }
+func randomHosts(n int, suffix string) []icinga.Host {
var hosts []icinga.Host
- for i := 0; i < 5; i++ {
+ for i := 0; i < n; i++ {
h := icinga.Host{
- Name: randomHostname(".example.org"),
+ Name: randomHostname(suffix),
CheckCommand: "random",
- Groups: []string{hostgroup.Name},
+ Groups: []string{"example"},
+ Address: randomTestAddr(),
}
hosts = append(hosts, h)
- if err := c.CreateHost(h); err != nil && !errors.Is(err, icinga.ErrExist) {
- return nil, err
- }
}
- return hosts, nil
+ return hosts
}
-func newTestClient() (*icinga.Client, error) {
+func newTestClient(t *testing.T) *icinga.Client {
tp := http.DefaultTransport.(*http.Transport)
tp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
- c := http.DefaultClient
- c.Transport = tp
- return icinga.Dial("127.0.0.1:5665", "root", "icinga", c)
+ c := &http.Client{Transport: tp}
+ if client, err := icinga.Dial("::1:5665", "icinga", "icinga", c); err == nil {
+ return client
+ }
+ client, err := icinga.Dial("127.0.0.1:5665", "icinga", "icinga", c)
+ if err == nil {
+ return client
+ }
+ t.Skipf("cannot dial local icinga: %v", err)
+ return nil
}
func compareStringSlice(a, b []string) bool {
}
func TestFilter(t *testing.T) {
- client, err := newTestClient()
- if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
- }
+ client := newTestClient(t)
+ var want, got []string // host names
- var want, got []string
- hosts, err := createTestHosts(client)
- if err != nil {
- t.Fatal(err)
+ hosts := randomHosts(10, "example.org")
+ for i := range hosts {
+ if err := client.CreateHost(hosts[i]); err != nil {
+ t.Fatal(err)
+ }
+ want = append(want, hosts[i].Name)
}
- for _, h := range hosts {
- want = append(want, h.Name)
- }
- defer func() {
- for _, h := range hosts {
- if err := client.DeleteHost(h.Name, true); err != nil {
- t.Log(err)
+ t.Cleanup(func() {
+ for i := range hosts {
+ if err := client.DeleteHost(hosts[i].Name, true); err != nil {
+ t.Error(err)
}
}
- }()
- hosts, err = client.Hosts("match(\"*example.org\", host.name)")
+ })
+
+ filter := `match("*example.org", host.name)`
+ hosts, err := client.Hosts(filter)
if err != nil {
t.Fatal(err)
}
- for _, h := range hosts {
- got = append(got, h.Name)
+ for i := range hosts {
+ got = append(got, hosts[i].Name)
}
+
sort.Strings(want)
sort.Strings(got)
if !compareStringSlice(want, got) {
- t.Fail()
+ t.Error("want", want, "got", got)
}
- t.Logf("want %+v got %+v", want, got)
}
func TestUserRoundTrip(t *testing.T) {
- client, err := newTestClient()
- if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
- }
+ client := newTestClient(t)
want := icinga.User{Name: "olly", Email: "olly@example.com", Groups: []string{}}
if err := client.CreateUser(want); err != nil && !errors.Is(err, icinga.ErrExist) {
t.Fatal(err)
}
func TestChecker(t *testing.T) {
- client, err := newTestClient()
- if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
- }
-
- h := icinga.Host{
- Name: randomHostname(".checker.example.com"),
- CheckCommand: "hostalive",
- }
+ client := newTestClient(t)
+ h := randomHosts(1, ".checker.example")[0]
if err := client.CreateHost(h); err != nil {
t.Fatal(err)
}
-
- s := icinga.Service{
+ defer client.DeleteHost(h.Name, true)
+ svc := icinga.Service{
Name: h.Name + "!http",
CheckCommand: "http",
}
- if err := client.CreateService(s); err != nil {
- t.Fatal(err)
+ if err := svc.Check(client); err == nil {
+ t.Error("nil error checking non-existent service")
}
- if err := s.Check(client); err != nil {
+ if err := client.CreateService(svc); err != nil {
t.Fatal(err)
}
- s, err = client.LookupService(h.Name + "!http")
- if err != nil {
- t.Fatal(err)
+ if err := svc.Check(client); err != nil {
+ t.Error(err)
}
- t.Logf("%+v\n", s)
+ if err := client.DeleteService(svc.Name, false); err != nil {
+ t.Error(err)
+ }
}
func TestCheckHostGroup(t *testing.T) {
- client, err := newTestClient()
- if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
+ client := newTestClient(t)
+ hostgroup := icinga.HostGroup{Name: "test", DisplayName: "Test Group"}
+ if err := client.CreateHostGroup(hostgroup); err != nil && !errors.Is(err, icinga.ErrExist) {
+ t.Fatal(err)
}
- hosts, err := createTestHosts(client)
+ defer client.DeleteHostGroup(hostgroup.Name, false)
+ hostgroup, err := client.LookupHostGroup(hostgroup.Name)
if err != nil {
t.Fatal(err)
}
- defer func() {
- for _, h := range hosts {
- if err := client.DeleteHost(h.Name, true); err != nil {
- t.Error(err)
- }
+ hosts := randomHosts(10, "example.org")
+ for _, h := range hosts {
+ h.Groups = []string{hostgroup.Name}
+ if err := client.CreateHost(h); err != nil {
+ t.Fatal(err)
}
- }()
- hostgroup, err := client.LookupHostGroup("test")
- if err != nil {
- t.Fatal(err)
+ defer client.DeleteHost(h.Name, false)
}
if err := hostgroup.Check(client); err != nil {
t.Fatal(err)
}
}
-func TestCreateService(t *testing.T) {
- client, err := newTestClient()
- if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
+func TestNonExistentService(t *testing.T) {
+ client := newTestClient(t)
+ filter := `match("blablabla", service.name)`
+ service, err := client.Services(filter)
+ if err == nil {
+ t.Error("non-nil error TODO")
+ t.Log(service)
}
+}
- h := icinga.Host{
- Name: "example.com",
- Address: "example.com",
- CheckCommand: "dummy",
- DisplayName: "RFC 2606 example host",
+type fakeServer struct {
+ objects map[string]attributes
+}
+
+func newFakeServer() *httptest.Server {
+ return httptest.NewTLSServer(&fakeServer{objects: make(map[string]attributes)})
+}
+
+// Returns an error message in the same format as returned by the Icinga2 API.
+func jsonError(err error) string {
+ return fmt.Sprintf("{ %q: %q }", "status", err.Error())
+}
+
+var notFoundResponse string = `{
+ "error": 404,
+ "status": "No objects found."
+}`
+
+var alreadyExistsResponse string = `
+{
+ "results": [
+ {
+ "code": 500,
+ "errors": [
+ "Object already exists."
+ ],
+ "status": "Object could not be created."
+ }
+ ]
+}`
+
+func (srv *fakeServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ if req.URL.RawQuery != "" {
+ http.Error(w, jsonError(errors.New("query parameters unimplemented")), http.StatusBadRequest)
+ return
}
- if err := client.CreateHost(h); err != nil {
- t.Error(err)
+
+ switch {
+ case path.Base(req.URL.Path) == "v1":
+ srv.Permissions(w)
+ return
+ case strings.HasPrefix(req.URL.Path, "/v1/objects"):
+ srv.ObjectsHandler(w, req)
+ return
}
- defer client.DeleteHost(h.Name, true)
- s := icinga.Service{
- Name: h.Name + "!http",
- CheckCommand: "http",
- DisplayName: "RFC 2606 example website",
+ http.Error(w, jsonError(errors.New(req.URL.Path+" unimplemented")), http.StatusNotFound)
+}
+
+func (f *fakeServer) Permissions(w http.ResponseWriter) {
+ file, err := os.Open("testdata/permissions.json")
+ if err != nil {
+ panic(err)
}
- if err := client.CreateService(s); err != nil {
- t.Error(err)
+ defer file.Close()
+ io.Copy(w, file)
+}
+
+type apiResponse struct {
+ Results []apiResult `json:"results"`
+ Status string `json:"status,omitempty"`
+}
+
+type apiResult struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Attrs attributes `json:"attrs"`
+}
+
+// attributes represent configuration object attributes
+type attributes map[string]interface{}
+
+// objType returns the icinga2 object type name from an API request path.
+// For example from "objects/services/test" the type name is "Service".
+func objType(path string) string {
+ var t string
+ a := strings.Split(path, "/")
+ for i := range a {
+ if a[i] == "objects" {
+ t = a[i+1] // services
+ }
}
+ return strings.TrimSuffix(strings.Title(t), "s") // Services to Service
}
-func TestNonExistentService(t *testing.T) {
- client, err := newTestClient()
+func (srv *fakeServer) ObjectsHandler(w http.ResponseWriter, req *http.Request) {
+ name := strings.TrimPrefix(req.URL.Path, "/v1/")
+ switch req.Method {
+ case http.MethodPut:
+ if _, ok := srv.objects[name]; ok {
+ http.Error(w, alreadyExistsResponse, http.StatusInternalServerError)
+ return
+ }
+ srv.CreateObject(w, req)
+ case http.MethodGet:
+ srv.GetObject(w, req)
+ case http.MethodDelete:
+ if _, ok := srv.objects[name]; !ok {
+ http.Error(w, notFoundResponse, http.StatusNotFound)
+ return
+ }
+ delete(srv.objects, name)
+ default:
+ err := fmt.Errorf("%s unimplemented", req.Method)
+ http.Error(w, jsonError(err), http.StatusMethodNotAllowed)
+ }
+}
+
+func (srv *fakeServer) GetObject(w http.ResponseWriter, req *http.Request) {
+ name := strings.TrimPrefix(req.URL.Path, "/v1/")
+ attrs, ok := srv.objects[name]
+ if !ok {
+ http.Error(w, notFoundResponse, http.StatusNotFound)
+ return
+ }
+ resp := apiResponse{
+ Results: []apiResult{
+ apiResult{
+ Name: path.Base(req.URL.Path),
+ Type: objType(req.URL.Path),
+ Attrs: attrs,
+ },
+ },
+ }
+ json.NewEncoder(w).Encode(&resp)
+}
+
+func (srv *fakeServer) CreateObject(w http.ResponseWriter, req *http.Request) {
+ defer req.Body.Close()
+ m := make(map[string]attributes)
+ if err := json.NewDecoder(req.Body).Decode(&m); err != nil {
+ panic(err)
+ }
+ name := strings.TrimPrefix(req.URL.Path, "/v1/")
+ srv.objects[name] = m["attrs"]
+}
+
+func TestDuplicateCreateDelete(t *testing.T) {
+ srv := newFakeServer()
+ defer srv.Close()
+ client, err := icinga.Dial(srv.Listener.Addr().String(), "root", "icinga", srv.Client())
if err != nil {
- t.Skipf("no local test icinga? got: %v", err)
+ t.Fatal(err)
}
- filter := `match("blablabla", service.name)`
- service, err := client.Services(filter)
- if err == nil {
- t.Fail()
+ host := randomHosts(1, ".example.org")[0]
+ if err := client.CreateHost(host); err != nil {
+ t.Fatal(err)
}
- t.Log(err)
- t.Logf("%+v", service)
+ if err := client.CreateHost(host); !errors.Is(err, icinga.ErrExist) {
+ t.Errorf("want %s got %v", icinga.ErrExist, err)
+ }
+ host, err = client.LookupHost(host.Name)
+ if err != nil {
+ t.Error(err)
+ }
+ if err := client.DeleteHost(host.Name, false); err != nil {
+ t.Error(err)
+ }
+ if err := client.DeleteHost(host.Name, false); !errors.Is(err, icinga.ErrNotExist) {
+ t.Errorf("want icinga.ErrNotExist got %s", err)
+ }
+ _, err = client.LookupHost(host.Name)
+ if !errors.Is(err, icinga.ErrNotExist) {
+ t.Errorf("want icinga.ErrNotExist got %s", err)
+ }
+ if err := client.CreateHost(host); err != nil {
+ t.Error(err)
+ }
+ host, err = client.LookupHost(host.Name)
+ if err != nil {
+ t.Error(err)
+ }
}