diff --git a/README.md b/README.md index d77cc79..9fdf303 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,23 @@ Example: command: /path/to/i3blocks/volume ``` +## Icinga + +Icinga2 plugin. Icinga2 must have API enabled and served on given URL + +Parameters: +```yaml + - name: icinga + plugin: icinga + config: + url: https://icinga/plugin/url + user: uber + pass: status_pass + # api_update_interval: 5m # defaults to 30s + # host_filter: icinga2 filter for hosts + # service_filter: icinga2 filter for services +``` + ## Uptime System uptime. Parameters: diff --git a/go.mod b/go.mod index 4d91a6c..8c032e4 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/efigence/go-icinga2 v0.2.0 // indirect github.com/efigence/go-libs v0.0.3 // indirect + github.com/efigence/go-monitoring v0.0.3 // indirect github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be // indirect github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect github.com/glycerine/greenpack v5.1.1+incompatible // indirect diff --git a/go.sum b/go.sum index b2037c9..0fe507c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/efigence/go-icinga2 v0.2.0 h1:2TAQTBGnte3BG1I0oSoUm0g/THBRBUCxz1zG90u6pIs= +github.com/efigence/go-icinga2 v0.2.0/go.mod h1:4+KJI7Lq/21EpyyPr0kjkF9cxozpCSNWPh4GuPYKCxA= github.com/efigence/go-libs v0.0.0-20171115061947-5b2936a0c2a1 h1:WJE5x8KYmcAW9U77NYb8WZBYvjjm9pjb7vS5TOePt7M= github.com/efigence/go-libs v0.0.0-20171115061947-5b2936a0c2a1/go.mod h1:+L8bBUFAIfgv+GWyTdOiHNUw9M1jh/7KxzmYXK2pUQA= github.com/efigence/go-libs v0.0.3 h1:lkvZAKB+bFWHYtvMm9nwbV8PBwBNQoRuoAbA1M8YPq0= @@ -18,6 +20,8 @@ github.com/efigence/go-mon v0.1.1 h1:PqkjJatL9gslvh5kfuop9vG0VwTBcwIL8epRzfLQJwY github.com/efigence/go-mon v0.1.1/go.mod h1:9RsOdOwCzexnFq0gqgi32gzj0jL3sfmz5gzU+lbhXx8= github.com/efigence/go-mon v1.4.2 h1:oT9TBMfHS16drXOpkI6G0jIpjKTemO1BG4GVMqv7yWI= github.com/efigence/go-mon v1.4.2/go.mod h1:7TP/HsWPDcoFOOPcEXwiknwAzSMGTGp0bvEuCH7froE= +github.com/efigence/go-monitoring v0.0.3 h1:HGROU4tGdtBgC7dP/As/x/Trb3cWMpiNiZrmKnE1wxo= +github.com/efigence/go-monitoring v0.0.3/go.mod h1:exrbNBDMWKiFIbppSFH4q+B+2dNYhDY0l+WxukXXOPY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= diff --git a/plugin/icinga/icinga.go b/plugin/icinga/icinga.go new file mode 100644 index 0000000..46ed1e0 --- /dev/null +++ b/plugin/icinga/icinga.go @@ -0,0 +1,216 @@ +package icinga + +import ( + "fmt" + "github.com/XANi/uberstatus/config" + "github.com/XANi/uberstatus/uber" + "github.com/XANi/uberstatus/util" + "github.com/efigence/go-icinga2" + "github.com/efigence/go-mon" + "go.uber.org/zap" + "sync" + + "time" +) + +// Icinga2 monitoring + +type pluginConfig struct { + Prefix string + Interval int + URL string `yaml:"url"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + HostFilter string `yaml:"host_filter"` + ServiceFilter string `yaml:"service_filter"` + APIUpdateInterval time.Duration `yaml:"api_update_interval"` +} + +type plugin struct { + l *zap.SugaredLogger + i *icinga2.API + cfg pluginConfig + hostStatusMap map[string]int + serviceStatusMap map[string]int + servicesDown []string + lastErr error + nextTs time.Time + sync.Mutex +} + +func New(cfg uber.PluginConfig) (z uber.Plugin, err error) { + p := &plugin{ + l: cfg.Logger, + } + p.cfg, err = loadConfig(cfg.Config) + return p, err +} + +func (p *plugin) Init() (err error) { + p.i, err = icinga2.New(p.cfg.URL, p.cfg.User, p.cfg.Pass) + if p.cfg.APIUpdateInterval < time.Second { + p.cfg.APIUpdateInterval = time.Second * 30 + } + p.hostStatusMap = map[string]int{ + "invalid": 0, + "up": 0, + "down": 0, + "unreachable": 0, + } + p.serviceStatusMap = map[string]int{ + "invalid": 0, + "ok": 0, + "warning": 0, + "critical": 0, + "unknown": 0, + } + + p.servicesDown = []string{} + w := make(chan bool) + go func() { + p.update() + w <- true + for { + time.Sleep(p.cfg.APIUpdateInterval) + p.update() + } + }() + // wait for a sec so we might get status right away instead of starting plugin with no state. + select { + case <-time.After(time.Second): + case <-w: + } + return err +} + +func (p *plugin) GetUpdateInterval() int { + return p.cfg.Interval +} +func (p *plugin) UpdatePeriodic() uber.Update { + var update uber.Update + tpl, _ := util.NewTemplate("uberEvent", `{{printf "%+v" .}}`) + // example on how to allow UpdateFromEvent to display for some time + // without being overwritten by periodic updates. + // We set up ts in our plugin, update it in UpdateFromEvent() and just wait if it is in future via helper function + util.WaitForTs(&p.nextTs) + p.Lock() + defer p.Unlock() + update.Markup = `pango` + if p.lastErr != nil { + tpl, _ = util.NewTemplate("uberEvent", `err {{.}}`) + update.FullText = tpl.ExecuteString(p.lastErr) + update.ShortText = fmt.Sprintf("%s", p.lastErr) + update.Color = `#ffcc66` + } else { + tpl, err := util.NewTemplate("uberEvent", `OK:{{ index . "ok"}} W:{{ index . "warning"}} C:{{ index . "critical" }}`) + if err != nil { + panic(err) + } + update.FullText = tpl.ExecuteString(p.serviceStatusMap) + update.ShortText = tpl.ExecuteString(p.serviceStatusMap) + if p.serviceStatusMap["critical"] > 0 { + update.Color = `#ff6666` + } else if p.serviceStatusMap["warning"] > 0 { + update.Color = `#cccc66` + } else if p.serviceStatusMap["unknown"] > 0 { + update.Color = `#6666cc` + } else { + update.Color = `#66cc66` + } + } + return update +} + +func (p *plugin) update() { + hosts, err := p.i.GetHostsByFilter(p.cfg.HostFilter) + statusCtr := map[uint8]int{} + for _, h := range hosts { + statusCtr[h.State]++ + } + statusMap := map[string]int{ + "invalid": statusCtr[uint8(mon.HostInvalid)], + "up": statusCtr[uint8(mon.HostUp)], + "down": statusCtr[uint8(mon.HostDown)], + "unreachable": statusCtr[uint8(mon.HostUnreachable)], + } + p.Lock() + p.hostStatusMap = statusMap + if err != nil { + p.lastErr = err + } + p.Unlock() + + services, err2 := p.i.GetServicesByFilter(p.cfg.ServiceFilter) + statusCtr = map[uint8]int{} + servicesNotOk := []string{} + for _, s := range services { + statusCtr[s.State]++ + if s.State != uint8(mon.StateOk) { + servicesNotOk = append(servicesNotOk, fmt.Sprintf("%s:%s", s.Host, s.Service)) + } + + } + statusMap = map[string]int{ + "invalid": statusCtr[uint8(mon.StateInvalid)], + "ok": statusCtr[uint8(mon.StateOk)], + "warning": statusCtr[uint8(mon.StateWarning)], + "critical": statusCtr[uint8(mon.StateCritical)], + "unknown": statusCtr[uint8(mon.StateUnknown)], + } + p.Lock() + p.serviceStatusMap = statusMap + p.servicesDown = servicesNotOk + if err != nil { + p.lastErr = err + } else { + p.lastErr = err2 + } + p.Unlock() +} + +func (p *plugin) UpdateFromEvent(e uber.Event) uber.Update { + var update uber.Update + tpl, _ := util.NewTemplate("uberEvent", `{{printf "%+v" .}}`) + // example on how to allow UpdateFromEvent to display for some time + // without being overwritten by periodic updates. + // We set up ts in our plugin, update it in UpdateFromEvent() and just wait if it is in future via helper function + util.WaitForTs(&p.nextTs) + p.Lock() + defer p.Unlock() + update.Markup = `pango` + if p.lastErr != nil { + tpl, _ = util.NewTemplate("uberEvent", `err {{.}}`) + update.FullText = tpl.ExecuteString(p.lastErr) + update.ShortText = fmt.Sprintf("%s", p.lastErr) + update.Color = `#ffcc66` + } else { + tpl, err := util.NewTemplate("uberEvent", `{{ printf "%+v" . }}`) + if err != nil { + panic(err) + } + update.FullText = tpl.ExecuteString(p.servicesDown) + update.ShortText = tpl.ExecuteString(p.servicesDown) + if p.serviceStatusMap["critical"] > 0 { + update.Color = `#ff6666` + } else if p.serviceStatusMap["warning"] > 0 { + update.Color = `#cccc66` + } else if p.serviceStatusMap["unknown"] > 0 { + update.Color = `#6666cc` + } else { + update.Color = `#66cc66` + } + } + + // set next TS updatePeriodic will wait to. + p.nextTs = time.Now().Add(time.Second * 3) + return update +} + +// parse received structure into pluginConfig +func loadConfig(c config.PluginConfig) (pluginConfig, error) { + var cfg pluginConfig + cfg.Interval = 10000 + cfg.Prefix = "ex: " + // optionally, check for pluginConfig validity after GetConfig call + return cfg, c.GetConfig(&cfg) +} diff --git a/plugin/plugin.go b/plugin/plugin.go index f39d7d4..8681be0 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/XANi/uberstatus/config" "github.com/XANi/uberstatus/plugin/gpu" + "github.com/XANi/uberstatus/plugin/icinga" "github.com/XANi/uberstatus/plugin/mqtt" "github.com/XANi/uberstatus/plugin/syncthing" @@ -36,6 +37,8 @@ var plugins = map[string]func(uber.PluginConfig) (uber.Plugin, error){ "example": example.New, "gpu": gpu.New, "i3blocks": i3blocks.New, + "icinga": icinga.New, + "icinga2": icinga.New, "memory": memory.New, "mqtt": mqtt.New, "network": network.New,