Implement minimal NUT client and use it instead of upsc

This commit is contained in:
Dominik Honnef
2016-12-27 23:22:48 +01:00
parent 6f026006c0
commit 2520206e7f
2 changed files with 152 additions and 33 deletions
+3 -2
View File
@@ -3,13 +3,14 @@ package main
import ( import (
"net/http" "net/http"
"honnef.co/go/nut"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"honnef.co/go/nut_exporter"
) )
func main() { func main() {
c := nut_exporter.NewNUTCollector([]string{"main"}, nil) c := nut.NewCollector([]string{"localhost"})
prometheus.MustRegister(c) prometheus.MustRegister(c)
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9999", nil) http.ListenAndServe(":9999", nil)
+149 -31
View File
@@ -1,15 +1,133 @@
package nut_exporter package nut
import ( import (
"bytes" "bufio"
"errors"
"fmt"
"log" "log"
"os/exec" "net"
"strconv" "strconv"
"strings" "strings"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
type Client struct {
conn net.Conn
br *bufio.Reader
}
func Dial(addr string) (*Client, error) {
_, _, err := net.SplitHostPort(addr)
if err != nil {
addr = net.JoinHostPort(addr, "3493")
}
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return NewClient(conn), nil
}
func NewClient(conn net.Conn) *Client {
return &Client{conn, bufio.NewReader(conn)}
}
func (c *Client) Close() error {
return c.conn.Close()
}
func (c *Client) list(typ string) ([]string, error) {
cmd := "LIST " + typ
if err := c.write(cmd); err != nil {
return nil, err
}
l, err := c.read()
if err != nil {
return nil, err
}
expected := "BEGIN " + cmd
if l != expected {
return nil, fmt.Errorf("expected %q, got %q", expected, l)
}
var lines []string
expected = typ + " "
for {
l, err := c.read()
if err != nil {
return nil, err
}
if l == "END "+cmd {
break
}
if !strings.HasPrefix(l, expected) {
return nil, fmt.Errorf("expected %q, got %q", expected, l)
}
l = l[len(expected):]
lines = append(lines, l)
}
return lines, nil
}
func (c *Client) UPSs() ([]string, error) {
lines, err := c.list("UPS")
if err != nil {
return nil, err
}
var upss []string
for _, l := range lines {
idx := strings.IndexByte(l, ' ')
if idx == -1 {
return nil, errors.New("protocol error")
}
ups := l[:idx]
upss = append(upss, ups)
}
return upss, nil
}
func (c *Client) Variables(ups string) (map[string]string, error) {
lines, err := c.list("VAR " + ups)
if err != nil {
return nil, err
}
vars := map[string]string{}
for _, l := range lines {
idx := strings.IndexByte(l, ' ')
if idx == -1 {
return nil, errors.New("protocol error")
}
k := l[:idx]
v := l[idx+1:]
v, err = strconv.Unquote(v)
if err != nil {
return nil, err
}
vars[k] = v
}
return vars, nil
}
func (c *Client) write(s string) error {
_, err := c.conn.Write([]byte(s + "\n"))
return err
}
func (c *Client) read() (string, error) {
l, err := c.br.ReadString('\n')
if err != nil {
return "", err
}
if len(l) > 0 {
l = l[:len(l)-1]
}
return l, nil
}
var descriptions = map[string]struct { var descriptions = map[string]struct {
name string name string
desc string desc string
@@ -86,7 +204,7 @@ var descriptions = map[string]struct {
"battery.packs.bad": {"battery_packs_bad", "Number of bad battery packs"}, "battery.packs.bad": {"battery_packs_bad", "Number of bad battery packs"},
} }
func NewNUTCollector(names []string, hosts []string) prometheus.Collector { func NewCollector(hosts []string) prometheus.Collector {
const namespace = "nut" const namespace = "nut"
descs := map[string]*prometheus.Desc{} descs := map[string]*prometheus.Desc{}
@@ -100,14 +218,12 @@ func NewNUTCollector(names []string, hosts []string) prometheus.Collector {
} }
return &nutCollector{ return &nutCollector{
names: names,
hosts: hosts, hosts: hosts,
descs: descs, descs: descs,
} }
} }
type nutCollector struct { type nutCollector struct {
names []string
hosts []string hosts []string
descs map[string]*prometheus.Desc descs map[string]*prometheus.Desc
} }
@@ -119,53 +235,55 @@ func (c *nutCollector) Describe(ch chan<- *prometheus.Desc) {
} }
func (c *nutCollector) Collect(ch chan<- prometheus.Metric) { func (c *nutCollector) Collect(ch chan<- prometheus.Metric) {
names := append(([]string)(nil), c.names...)
for _, host := range c.hosts { for _, host := range c.hosts {
hn, err := c.getNUTNames(host) conn, err := Dial(host)
if err != nil { if err != nil {
log.Printf("error connecting to NUT server: %s", err)
continue continue
} }
names = append(names, hn...) upss, err := conn.UPSs()
} if err != nil {
for _, name := range names { log.Printf("error getting list of UPSs: %s", err)
if err := c.readNUT(name, ch); err != nil { _ = conn.Close()
log.Printf("error reading UPS values: %s", err) continue
} }
for _, ups := range upss {
if err := c.readNUT(conn, ups, ch); err != nil {
log.Printf("error reading UPS values: %s", err)
}
}
_ = conn.Close()
} }
} }
func parseName(s string) (name string, host string) {
idx := strings.IndexByte(s, '@')
if idx == -1 {
return s, "localhost:3439"
}
return s[:idx], s[idx+1:]
}
func (c *nutCollector) getNUTNames(host string) ([]string, error) { func (c *nutCollector) getNUTNames(host string) ([]string, error) {
cmd := exec.Command("upsc", "-l", host) conn, err := Dial(host)
out, err := cmd.Output()
if err != nil { if err != nil {
return nil, err return nil, err
} }
names := strings.Split(string(out), "\n") defer conn.Close()
if len(names) > 0 { return conn.UPSs()
names = names[:len(names)-1]
}
return names, nil
} }
func (c *nutCollector) readNUT(name string, ch chan<- prometheus.Metric) error { func (c *nutCollector) readNUT(conn *Client, name string, ch chan<- prometheus.Metric) error {
cmd := exec.Command("upsc", name) vars, err := conn.Variables(name)
out, err := cmd.Output()
if err != nil { if err != nil {
return err return err
} }
labels := map[string]string{} labels := map[string]string{}
values := map[string]float64{} values := map[string]float64{}
for k := range descriptions { for k := range descriptions {
values[k] = 0 values[k] = 0
} }
for _, l := range bytes.Split(out, []byte("\n")) { for k, v := range vars {
parts := bytes.SplitN(l, []byte(": "), 2)
if len(parts) != 2 {
continue
}
k, v := string(parts[0]), string(parts[1])
switch k { switch k {
case "device.model", "device.mfr", "device.serial", "device.type": case "device.model", "device.mfr", "device.serial", "device.type":
labels[k] = v labels[k] = v