mirror of
https://github.com/rwinkhart/uni-nut.git
synced 2026-09-05 16:37:15 -04:00
Implement minimal NUT client and use it instead of upsc
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user