icinga

Icinga2 API client in Go
git clone https://git.olowe.co/icinga
Log | Files | Refs | README | LICENSE

icinga_test.go (8751B)


      1 package icinga_test
      2 
      3 import (
      4 	"crypto/tls"
      5 	"encoding/json"
      6 	"errors"
      7 	"fmt"
      8 	"math/rand"
      9 	"net/http"
     10 	"net/http/httptest"
     11 	"path"
     12 	"reflect"
     13 	"sort"
     14 	"strings"
     15 	"testing"
     16 	"time"
     17 
     18 	"olowe.co/icinga"
     19 )
     20 
     21 func init() {
     22 	rand.Seed(time.Now().Unix())
     23 }
     24 
     25 func randomHostname(suffix string) string {
     26 	var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
     27 	b := make([]rune, 8)
     28 	for i := range b {
     29 		b[i] = letters[rand.Intn(len(letters))]
     30 	}
     31 	return string(b) + suffix
     32 }
     33 
     34 func randomTestAddr() string { return fmt.Sprintf("192.0.2.%d", rand.Intn(254)) }
     35 
     36 func randomHosts(n int, suffix string) []icinga.Host {
     37 	var hosts []icinga.Host
     38 	for i := 0; i < n; i++ {
     39 		h := icinga.Host{
     40 			Name:         randomHostname(suffix),
     41 			CheckCommand: "random",
     42 			Address:      randomTestAddr(),
     43 		}
     44 		hosts = append(hosts, h)
     45 	}
     46 	return hosts
     47 }
     48 
     49 func newTestClient(t *testing.T) *icinga.Client {
     50 	tp := http.DefaultTransport.(*http.Transport)
     51 	tp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
     52 	c := &http.Client{Transport: tp}
     53 	if client, err := icinga.Dial("::1:5665", "icinga", "icinga", c); err == nil {
     54 		return client
     55 	}
     56 	client, err := icinga.Dial("127.0.0.1:5665", "icinga", "icinga", c)
     57 	if err == nil {
     58 		return client
     59 	}
     60 	t.Skipf("cannot dial local icinga: %v", err)
     61 	return nil
     62 }
     63 
     64 func compareStringSlice(a, b []string) bool {
     65 	if len(a) != len(b) {
     66 		return false
     67 	}
     68 	for i := range a {
     69 		if a[i] != b[i] {
     70 			return false
     71 		}
     72 	}
     73 	return true
     74 }
     75 
     76 func TestFilter(t *testing.T) {
     77 	client := newTestClient(t)
     78 	var want, got []string // host names
     79 
     80 	hosts := randomHosts(10, "example.org")
     81 	for i := range hosts {
     82 		if err := client.CreateHost(hosts[i]); err != nil {
     83 			t.Fatal(err)
     84 		}
     85 		want = append(want, hosts[i].Name)
     86 	}
     87 	t.Cleanup(func() {
     88 		for i := range hosts {
     89 			if err := client.DeleteHost(hosts[i].Name, true); err != nil {
     90 				t.Error(err)
     91 			}
     92 		}
     93 	})
     94 
     95 	filter := `match("*example.org", host.name)`
     96 	hosts, err := client.Hosts(filter)
     97 	if err != nil {
     98 		t.Fatal(err)
     99 	}
    100 	for i := range hosts {
    101 		got = append(got, hosts[i].Name)
    102 	}
    103 
    104 	sort.Strings(want)
    105 	sort.Strings(got)
    106 	if !compareStringSlice(want, got) {
    107 		t.Error("want", want, "got", got)
    108 	}
    109 }
    110 
    111 func TestUserRoundTrip(t *testing.T) {
    112 	client := newTestClient(t)
    113 	want := icinga.User{Name: "olly", Email: "olly@example.com", Groups: []string{}}
    114 	if err := client.CreateUser(want); err != nil && !errors.Is(err, icinga.ErrExist) {
    115 		t.Fatal(err)
    116 	}
    117 	defer func() {
    118 		if err := client.DeleteUser(want.Name, false); err != nil {
    119 			t.Error(err)
    120 		}
    121 	}()
    122 	got, err := client.LookupUser(want.Name)
    123 	if err != nil {
    124 		t.Fatal(err)
    125 	}
    126 	if !reflect.DeepEqual(want, got) {
    127 		t.Errorf("want %+v, got %+v", want, got)
    128 	}
    129 }
    130 
    131 func TestChecker(t *testing.T) {
    132 	client := newTestClient(t)
    133 	h := randomHosts(1, ".checker.example")[0]
    134 	if err := client.CreateHost(h); err != nil {
    135 		t.Fatal(err)
    136 	}
    137 	defer client.DeleteHost(h.Name, true)
    138 	svc := icinga.Service{
    139 		Name:         h.Name + "!http",
    140 		CheckCommand: "http",
    141 	}
    142 	if err := svc.Check(client); err == nil {
    143 		t.Error("nil error checking non-existent service")
    144 	}
    145 	if err := client.CreateService(svc); err != nil {
    146 		t.Fatal(err)
    147 	}
    148 	if err := svc.Check(client); err != nil {
    149 		t.Error(err)
    150 	}
    151 	if err := client.DeleteService(svc.Name, false); err != nil {
    152 		t.Error(err)
    153 	}
    154 }
    155 
    156 func TestCheckHostGroup(t *testing.T) {
    157 	client := newTestClient(t)
    158 	hostgroup := icinga.HostGroup{Name: "test", DisplayName: "Test Group"}
    159 	if err := client.CreateHostGroup(hostgroup); err != nil && !errors.Is(err, icinga.ErrExist) {
    160 		t.Fatal(err)
    161 	}
    162 	defer client.DeleteHostGroup(hostgroup.Name, false)
    163 	hostgroup, err := client.LookupHostGroup(hostgroup.Name)
    164 	if err != nil {
    165 		t.Fatal(err)
    166 	}
    167 	hosts := randomHosts(10, "example.org")
    168 	for _, h := range hosts {
    169 		h.Groups = []string{hostgroup.Name}
    170 		if err := client.CreateHost(h); err != nil {
    171 			t.Fatal(err)
    172 		}
    173 		defer client.DeleteHost(h.Name, false)
    174 	}
    175 	if err := hostgroup.Check(client); err != nil {
    176 		t.Fatal(err)
    177 	}
    178 }
    179 
    180 func TestNonExistentService(t *testing.T) {
    181 	client := newTestClient(t)
    182 	filter := `match("blablabla", service.name)`
    183 	service, err := client.Services(filter)
    184 	if err == nil {
    185 		t.Error("non-nil error TODO")
    186 		t.Log(service)
    187 	}
    188 }
    189 
    190 type fakeServer struct {
    191 	objects map[string]attributes
    192 }
    193 
    194 func newFakeServer() *httptest.Server {
    195 	return httptest.NewTLSServer(&fakeServer{objects: make(map[string]attributes)})
    196 }
    197 
    198 // Returns an error message in the same format as returned by the Icinga2 API.
    199 func jsonError(err error) string {
    200 	return fmt.Sprintf("{ %q: %q }", "status", err.Error())
    201 }
    202 
    203 var notFoundResponse string = `{
    204     "error": 404,
    205     "status": "No objects found."
    206 }`
    207 
    208 var alreadyExistsResponse string = `
    209 {
    210     "results": [
    211         {
    212             "code": 500,
    213             "errors": [
    214                 "Object already exists."
    215             ],
    216             "status": "Object could not be created."
    217         }
    218     ]
    219 }`
    220 
    221 func (srv *fakeServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    222 	if req.URL.RawQuery != "" {
    223 		http.Error(w, jsonError(errors.New("query parameters unimplemented")), http.StatusBadRequest)
    224 		return
    225 	}
    226 
    227 	switch {
    228 	case path.Base(req.URL.Path) == "v1":
    229 		srv.Permissions(w)
    230 		return
    231 	case strings.HasPrefix(req.URL.Path, "/v1/objects"):
    232 		srv.ObjectsHandler(w, req)
    233 		return
    234 	}
    235 	http.Error(w, jsonError(errors.New(req.URL.Path+" unimplemented")), http.StatusNotFound)
    236 }
    237 
    238 func (f *fakeServer) Permissions(w http.ResponseWriter) {
    239 	fmt.Fprint(w, `{"results": [{
    240 		"info": "Fake Icinga2 server",
    241 		"permissions": ["*"],
    242 		"user": "icinga",
    243 		"version": "fake"
    244 	}]}`)
    245 
    246 }
    247 
    248 type apiResponse struct {
    249 	Results []apiResult `json:"results"`
    250 	Status  string      `json:"status,omitempty"`
    251 }
    252 
    253 type apiResult struct {
    254 	Name  string     `json:"name"`
    255 	Type  string     `json:"type"`
    256 	Attrs attributes `json:"attrs"`
    257 }
    258 
    259 // attributes represent configuration object attributes
    260 type attributes map[string]interface{}
    261 
    262 // objType returns the icinga2 object type name from an API request path.
    263 // For example from "objects/services/test" the type name is "Service".
    264 func objType(path string) string {
    265 	var t string
    266 	a := strings.Split(path, "/")
    267 	for i := range a {
    268 		if a[i] == "objects" {
    269 			t = a[i+1] // services
    270 		}
    271 	}
    272 	return strings.TrimSuffix(strings.Title(t), "s") // Services to Service
    273 }
    274 
    275 func (srv *fakeServer) ObjectsHandler(w http.ResponseWriter, req *http.Request) {
    276 	name := strings.TrimPrefix(req.URL.Path, "/v1/")
    277 	switch req.Method {
    278 	case http.MethodPut:
    279 		if _, ok := srv.objects[name]; ok {
    280 			http.Error(w, alreadyExistsResponse, http.StatusInternalServerError)
    281 			return
    282 		}
    283 		srv.CreateObject(w, req)
    284 	case http.MethodGet:
    285 		srv.GetObject(w, req)
    286 	case http.MethodDelete:
    287 		if _, ok := srv.objects[name]; !ok {
    288 			http.Error(w, notFoundResponse, http.StatusNotFound)
    289 			return
    290 		}
    291 		delete(srv.objects, name)
    292 	default:
    293 		err := fmt.Errorf("%s unimplemented", req.Method)
    294 		http.Error(w, jsonError(err), http.StatusMethodNotAllowed)
    295 	}
    296 }
    297 
    298 func (srv *fakeServer) GetObject(w http.ResponseWriter, req *http.Request) {
    299 	name := strings.TrimPrefix(req.URL.Path, "/v1/")
    300 	attrs, ok := srv.objects[name]
    301 	if !ok {
    302 		http.Error(w, notFoundResponse, http.StatusNotFound)
    303 		return
    304 	}
    305 	resp := apiResponse{
    306 		Results: []apiResult{
    307 			apiResult{
    308 				Name:  path.Base(req.URL.Path),
    309 				Type:  objType(req.URL.Path),
    310 				Attrs: attrs,
    311 			},
    312 		},
    313 	}
    314 	json.NewEncoder(w).Encode(&resp)
    315 }
    316 
    317 func (srv *fakeServer) CreateObject(w http.ResponseWriter, req *http.Request) {
    318 	defer req.Body.Close()
    319 	m := make(map[string]attributes)
    320 	if err := json.NewDecoder(req.Body).Decode(&m); err != nil {
    321 		panic(err)
    322 	}
    323 	name := strings.TrimPrefix(req.URL.Path, "/v1/")
    324 	srv.objects[name] = m["attrs"]
    325 }
    326 
    327 func TestDuplicateCreateDelete(t *testing.T) {
    328 	srv := newFakeServer()
    329 	defer srv.Close()
    330 	client, err := icinga.Dial(srv.Listener.Addr().String(), "root", "icinga", srv.Client())
    331 	if err != nil {
    332 		t.Fatal(err)
    333 	}
    334 
    335 	host := randomHosts(1, ".example.org")[0]
    336 	if err := client.CreateHost(host); err != nil {
    337 		t.Fatal(err)
    338 	}
    339 	if err := client.CreateHost(host); !errors.Is(err, icinga.ErrExist) {
    340 		t.Errorf("want %s got %v", icinga.ErrExist, err)
    341 	}
    342 	host, err = client.LookupHost(host.Name)
    343 	if err != nil {
    344 		t.Error(err)
    345 	}
    346 	if err := client.DeleteHost(host.Name, false); err != nil {
    347 		t.Error(err)
    348 	}
    349 	if err := client.DeleteHost(host.Name, false); !errors.Is(err, icinga.ErrNotExist) {
    350 		t.Errorf("want icinga.ErrNotExist got %s", err)
    351 	}
    352 	_, err = client.LookupHost(host.Name)
    353 	if !errors.Is(err, icinga.ErrNotExist) {
    354 		t.Errorf("want icinga.ErrNotExist got %s", err)
    355 	}
    356 	if err := client.CreateHost(host); err != nil {
    357 		t.Error(err)
    358 	}
    359 	host, err = client.LookupHost(host.Name)
    360 	if err != nil {
    361 		t.Error(err)
    362 	}
    363 }