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 }