icinga

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

commit 26fb88e9ebc4192da36411de66032158b424369d
parent 82fc97ff8eb537c28bce19af5ffb697773bd200c
Author: Oliver Lowe <o@olowe.co>
Date:   Wed, 12 Jan 2022 20:18:00 +1100

Generate CRUD code using go generate

Explanation for use is in the README

Diffstat:
MREADME.md | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrud.awk | 6++++++
Acrud.go | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrud.sh | 45+++++++++++++++++++++++++++++++++++++++++++++
Acrud.skel | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mhost.go | 56+-------------------------------------------------------
Mobject.go | 4+++-
Mservice.go | 24+-----------------------
Muser.go | 51+--------------------------------------------------
9 files changed, 327 insertions(+), 129 deletions(-)

diff --git a/README.md b/README.md @@ -2,6 +2,10 @@ package icinga provides a client to the Icinga2 HTTP API. [![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]. diff --git a/crud.awk b/crud.awk @@ -0,0 +1,6 @@ +{ + gsub("TYPE", type) + gsub("LOWER", tolower(type)) + gsub("PLURAL", tolower(type) "s") + print +} diff --git a/crud.go b/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 +} + diff --git a/crud.sh b/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 diff --git a/crud.skel b/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 +} + diff --git a/host.go b/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 -} diff --git a/object.go b/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 { diff --git a/service.go b/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 -} diff --git a/user.go b/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 -}