Commit Diff


commit - 82fc97ff8eb537c28bce19af5ffb697773bd200c
commit + 26fb88e9ebc4192da36411de66032158b424369d
blob - 78f3060f0579f6a1d8864e12180a78cf336c87ba
blob + 0f4a86c710832cc1ac128db30560d8b871d6635a
--- README.md
+++ README.md
@@ -2,6 +2,10 @@ package icinga provides a client to the Icinga2 HTTP A
 
 [![godocs.io](http://godocs.io/olowe.co/icinga?status.svg)](http://godocs.io/olowe.co/icinga)
 
+Send any patches, questions or requests to the [mailing list][list] [~otl/icinga@lists.sr.ht](mailto:~otl/icinga@lists.sr.ht) 🙂
+
+[list]: https://lists.sr.ht/~otl/icinga
+
 ## Quick Start
 
 A Client manages interaction with an Icinga2 server.
@@ -46,6 +50,62 @@ For more detail, see the [godocs][godocs].
 
 [godocs]: https://godocs.io/olowe.co/icinga
 
+## Development
+
+Some code is automatically generated. Ensure it's up-to-date before starting work:
+
+	go generate
+
+Make some changes, then run the tests. No fancy test framework:
+
+	go test
+
+Send any questions, patches, requests to the mailing list [~otl/icinga@lists.sr.ht](mailto:~otl/icinga@lists.sr.ht)
+
+### Testing
+
+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:
+
+	object ApiUser "root" {
+		password = "icinga"
+		permissions = [ "*" ]
+	}
+
+Getting data from 127.0.0.1:5665 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].
+
+[image]: https://hub.docker.com/r/icinga/icinga2
+
+### Code generation
+
+Source code for the basic lookup, create and delete operations of some
+Icinga2 object types, such as Host and Service, are generated
+automatically.
+
+The shell script crud.sh writes Go source code by reading a template
+file and doing some text substitution. It loops through object types,
+piping the template file crud.skel into the awk script crud.awk for
+each.
+
+crud.sh writes code to the standard output by default:
+
+	./crud.sh
+
+If the flag `-o` is set, code will be written to the file
+specified instead of to standard output:
+
+	./crud.sh -o crud.go
+
+Code generation is used because the functions are trivial and call the exact
+same underlying methods on Client anyway. The only thing that differs is the type.
+Perhaps when Go gets type parameters then this will go away?
+
 ## Why Another Package?
 
 The [icinga2 terraform provider][tf] uses the package [github.com/lrsmith/go-icinga2-api/iapi][lrsmith].
blob - /dev/null
blob + 57745a77e751377bfb9591ab85be6498a03c4c39 (mode 644)
--- /dev/null
+++ crud.awk
@@ -0,0 +1,6 @@
+{
+	gsub("TYPE", type)
+	gsub("LOWER", tolower(type))
+	gsub("PLURAL", tolower(type) "s")
+	print
+}
blob - d65e59b24fb1e7f2a481c9205a661f34ad299e46
blob + c91758f751aced6fa19666b0f27ba3d014a2446a
--- host.go
+++ host.go
@@ -1,9 +1,6 @@
 package icinga
 
-import (
-	"encoding/json"
-	"fmt"
-)
+import "encoding/json"
 
 // Host represents a Host object.
 type Host struct {
@@ -68,54 +65,3 @@ func (h Host) MarshalJSON() ([]byte, error) {
 	}
 	return json.Marshal(jhost)
 }
-
-// Hosts returns Hosts matching the filter expression filter.
-// If no hosts match, error wraps ErrNoMatch.
-// To fetch all hosts, set filter to the empty string ("").
-func (c *Client) Hosts(filter string) ([]Host, error) {
-	objects, err := c.filterObjects("/objects/hosts", filter)
-	if err != nil {
-		return nil, fmt.Errorf("get hosts filter %q: %w", filter, 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)
-	}
-	return hosts, nil
-}
-
-// LookupHost returns the Host identified by name. If no Host is found,
-// error wraps ErrNotExist.
-func (c *Client) LookupHost(name string) (Host, error) {
-	obj, err := c.lookupObject("/objects/hosts/" + name)
-	if err != nil {
-		return Host{}, fmt.Errorf("lookup %s: %w", name, err)
-	}
-	v, ok := obj.(Host)
-	if !ok {
-		return Host{}, fmt.Errorf("lookup %s: result type %T is not host", name, obj)
-	}
-	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 {
-	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 ErrNotExist.
-func (c *Client) DeleteHost(name string) error {
-	if err := c.deleteObject("/objects/hosts/" + name); err != nil {
-		return fmt.Errorf("delete host %s: %w", name, err)
-	}
-	return nil
-}
blob - /dev/null
blob + a11f3493120c3b4c35c45f48922a3837c1ebf953 (mode 644)
--- /dev/null
+++ crud.go
@@ -0,0 +1,159 @@
+// Code generated by ./crud.sh ; DO NOT EDIT.
+
+package icinga
+
+import "fmt"
+
+// Hosts returns a slice of Host matching the filter expression filter.
+// If no hosts match, error wraps ErrNoMatch.
+// To fetch all host, set filter to the empty string ("").
+func (c *Client) Hosts(filter string) ([]Host, error) {
+	objects, err := c.filterObjects("/objects/hosts", filter)
+	if err != nil {
+		return nil, fmt.Errorf("get hosts filter %q: %w", filter, err)
+	}
+	var hosts []Host
+	for _, o := range objects {
+		v, ok := o.(Host)
+		if !ok {
+			return nil, fmt.Errorf("get hosts filter %q: %T in response", filter, v)
+		}
+		hosts = append(hosts, v)
+	}
+	return hosts, nil
+}
+
+// LookupHost returns the Host identified by name. If no Host is found, error
+// wraps ErrNotExist.
+func (c *Client) LookupHost(name string) (Host, error) {
+	obj, err := c.lookupObject("/objects/hosts/" + name)
+	if err != nil {
+		return Host{}, fmt.Errorf("lookup host %s: %w", name, err)
+	}
+	v, ok := obj.(Host)
+	if !ok {
+		return Host{}, fmt.Errorf("lookup host %s: result type %T is not Host", name, v)
+	}
+	return v, nil
+}
+
+// CreateHost creates host. Some fields of host must be set for successful
+// creation; see the type definition of Host for details.
+func (c *Client) CreateHost(host Host) error {
+	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 ErrNotExist.
+func (c *Client) DeleteHost(name string) error {
+	if err := c.deleteObject("/objects/hosts/" + name); err != nil {
+		return fmt.Errorf("delete host %s: %w", name, err)
+	}
+	return nil
+}
+
+// Services returns a slice of Service matching the filter expression filter.
+// If no hosts match, error wraps ErrNoMatch.
+// To fetch all service, set filter to the empty string ("").
+func (c *Client) Services(filter string) ([]Service, error) {
+	objects, err := c.filterObjects("/objects/services", filter)
+	if err != nil {
+		return nil, fmt.Errorf("get services filter %q: %w", filter, err)
+	}
+	var services []Service
+	for _, o := range objects {
+		v, ok := o.(Service)
+		if !ok {
+			return nil, fmt.Errorf("get services filter %q: %T in response", filter, v)
+		}
+		services = append(services, v)
+	}
+	return services, nil
+}
+
+// LookupService returns the Service identified by name. If no Service is found, error
+// wraps ErrNotExist.
+func (c *Client) LookupService(name string) (Service, error) {
+	obj, err := c.lookupObject("/objects/services/" + name)
+	if err != nil {
+		return Service{}, fmt.Errorf("lookup service %s: %w", name, err)
+	}
+	v, ok := obj.(Service)
+	if !ok {
+		return Service{}, fmt.Errorf("lookup service %s: result type %T is not Service", name, v)
+	}
+	return v, nil
+}
+
+// CreateService creates service. Some fields of service must be set for successful
+// creation; see the type definition of Service for details.
+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
+}
+
+// DeleteService deletes the Service identified by name.
+// If no Service is found, error wraps ErrNotExist.
+func (c *Client) DeleteService(name string) error {
+	if err := c.deleteObject("/objects/services/" + name); err != nil {
+		return fmt.Errorf("delete service %s: %w", name, err)
+	}
+	return nil
+}
+
+// Users returns a slice of User matching the filter expression filter.
+// If no hosts match, error wraps ErrNoMatch.
+// To fetch all user, set filter to the empty string ("").
+func (c *Client) Users(filter string) ([]User, error) {
+	objects, err := c.filterObjects("/objects/users", filter)
+	if err != nil {
+		return nil, fmt.Errorf("get users filter %q: %w", filter, err)
+	}
+	var users []User
+	for _, o := range objects {
+		v, ok := o.(User)
+		if !ok {
+			return nil, fmt.Errorf("get users filter %q: %T in response", filter, v)
+		}
+		users = append(users, v)
+	}
+	return users, nil
+}
+
+// LookupUser returns the User identified by name. If no User is found, error
+// wraps ErrNotExist.
+func (c *Client) LookupUser(name string) (User, error) {
+	obj, err := c.lookupObject("/objects/users/" + name)
+	if err != nil {
+		return User{}, fmt.Errorf("lookup user %s: %w", name, err)
+	}
+	v, ok := obj.(User)
+	if !ok {
+		return User{}, fmt.Errorf("lookup user %s: result type %T is not User", name, v)
+	}
+	return v, nil
+}
+
+// CreateUser creates user. Some fields of user must be set for successful
+// creation; see the type definition of User for details.
+func (c *Client) CreateUser(user User) error {
+	if err := c.createObject(user); err != nil {
+		return fmt.Errorf("create user %s: %w", user.Name, err)
+	}
+	return nil
+}
+
+// DeleteUser deletes the User identified by name.
+// If no User is found, error wraps ErrNotExist.
+func (c *Client) DeleteUser(name string) error {
+	if err := c.deleteObject("/objects/users/" + name); err != nil {
+		return fmt.Errorf("delete user %s: %w", name, err)
+	}
+	return nil
+}
+
blob - /dev/null
blob + c2fa08026c533355acbe737f3b03804ce4f614cc (mode 755)
--- /dev/null
+++ crud.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+types="Host Service User"
+
+args=`getopt o: $*`
+if test $? -ne 0
+then
+	echo "usage: lookupgen.sh [-o file]"
+	exit 2
+fi
+set -- $args
+while test "$#" -ne 0
+do
+	case "$1"
+	in
+		-o)
+			file="$2"; shift; shift;;
+		--)
+			shift; break;;
+	esac
+done
+
+head="// Code generated by $0 $@; DO NOT EDIT.
+
+package icinga
+
+import \"fmt\"
+"
+
+if test -n "$file"
+then
+	echo "$head" > "$file"
+else
+	echo "$head"
+fi
+
+for t in $types
+do
+	if test -n "$file"
+	then
+		awk -v "type=$t" -f crud.awk crud.skel >> "$file"
+	else
+		awk -v "type=$t" -f crud.awk crud.skel
+	fi
+done
blob - /dev/null
blob + cf986a305b5ea0e52dc58d6de5e4a6469b80f18a (mode 644)
--- /dev/null
+++ crud.skel
@@ -0,0 +1,51 @@
+// TYPEs returns a slice of TYPE matching the filter expression filter.
+// If no hosts match, error wraps ErrNoMatch.
+// To fetch all LOWER, set filter to the empty string ("").
+func (c *Client) TYPEs(filter string) ([]TYPE, error) {
+	objects, err := c.filterObjects("/objects/PLURAL", filter)
+	if err != nil {
+		return nil, fmt.Errorf("get PLURAL filter %q: %w", filter, err)
+	}
+	var PLURAL []TYPE
+	for _, o := range objects {
+		v, ok := o.(TYPE)
+		if !ok {
+			return nil, fmt.Errorf("get PLURAL filter %q: %T in response", filter, v)
+		}
+		PLURAL = append(PLURAL, v)
+	}
+	return PLURAL, nil
+}
+
+// LookupTYPE returns the TYPE identified by name. If no TYPE is found, error
+// wraps ErrNotExist.
+func (c *Client) LookupTYPE(name string) (TYPE, error) {
+	obj, err := c.lookupObject("/objects/PLURAL/" + name)
+	if err != nil {
+		return TYPE{}, fmt.Errorf("lookup LOWER %s: %w", name, err)
+	}
+	v, ok := obj.(TYPE)
+	if !ok {
+		return TYPE{}, fmt.Errorf("lookup LOWER %s: result type %T is not TYPE", name, v)
+	}
+	return v, nil
+}
+
+// CreateTYPE creates LOWER. Some fields of LOWER must be set for successful
+// creation; see the type definition of TYPE for details.
+func (c *Client) CreateTYPE(LOWER TYPE) error {
+	if err := c.createObject(LOWER); err != nil {
+		return fmt.Errorf("create LOWER %s: %w", LOWER.Name, err)
+	}
+	return nil
+}
+
+// DeleteTYPE deletes the TYPE identified by name.
+// If no TYPE is found, error wraps ErrNotExist.
+func (c *Client) DeleteTYPE(name string) error {
+	if err := c.deleteObject("/objects/PLURAL/" + name); err != nil {
+		return fmt.Errorf("delete LOWER %s: %w", name, err)
+	}
+	return nil
+}
+
blob - a09a08269b2f988f03f4099e057e222fde2177ac
blob + 72cb8f1953b6a3cdff843006531f78c4a0895fa8
--- object.go
+++ object.go
@@ -1,3 +1,4 @@
+
 package icinga
 
 import (
@@ -11,10 +12,11 @@ import (
 
 type object interface {
 	name() string
-	attrs() map[string]interface{}
 	path() string
 }
 
+//go:generate ./crud.sh -o crud.go
+
 func (c *Client) lookupObject(objpath string) (object, error) {
 	resp, err := c.get(objpath, "")
 	if err != nil {
blob - 8d8a80f0fe3ffcf01b3a2054aaf32e0b6a8dccbf
blob + 57ff914a3e5b3f142c17ae0e797b260301b9233d
--- service.go
+++ service.go
@@ -1,9 +1,6 @@
 package icinga
 
-import (
-	"encoding/json"
-	"fmt"
-)
+import "encoding/json"
 
 func (s Service) name() string {
 	return s.Name
@@ -63,22 +60,3 @@ func (s Service) MarshalJSON() ([]byte, error) {
 	}{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
-}
blob - 1ad4fa1f9efbf7d447b2dbac6ea29dbda6992300
blob + b9c85caf88e41b5f1c13adf09e932558615f949f
--- user.go
+++ user.go
@@ -1,9 +1,6 @@
 package icinga
 
-import (
-	"encoding/json"
-	"fmt"
-)
+import "encoding/json"
 
 // User represents a User object.
 // Note that this is different from an ApiUser.
@@ -47,49 +44,3 @@ func (u User) attrs() map[string]interface{} {
 	m["email"] = u.Email
 	return m
 }
-
-func (c *Client) Users() ([]User, error) {
-	objects, err := c.filterObjects("/objects/users", "")
-	if err != nil {
-		return nil, fmt.Errorf("get all users: %w", err)
-	}
-	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) {
-	obj, err := c.lookupObject("/objects/users/" + name)
-	if err != nil {
-		return User{}, fmt.Errorf("lookup %s: %w", name, err)
-	}
-	v, ok := obj.(User)
-	if !ok {
-		return User{}, fmt.Errorf("lookup %s: result type %T is not user", name, v)
-	}
-	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 {
-	if err := c.createObject(user); err != nil {
-		return fmt.Errorf("create user %s: %w", user.Name, err)
-	}
-	return nil
-}
-
-// DeleteUser deletes the User identified by name.
-// ErrNotExist is returned if the User doesn't exist.
-func (c *Client) DeleteUser(name string) error {
-	if err := c.deleteObject("/objects/users/" + name); err != nil {
-		return fmt.Errorf("delete user %s: %w", name, err)
-	}
-	return nil
-}