firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/upsd/client.go

Summary

Maintainability
A
35 mins
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package upsd

import (
    "encoding/csv"
    "errors"
    "fmt"
    "strings"

    "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
)

const (
    commandUsername = "USERNAME %s"
    commandPassword = "PASSWORD %s"
    commandListUPS  = "LIST UPS"
    commandListVar  = "LIST VAR %s"
    commandLogout   = "LOGOUT"
)

// https://github.com/networkupstools/nut/blob/81fca30b2998fa73085ce4654f075605ff0b9e01/docs/net-protocol.txt#L647
var errUpsdCommand = errors.New("upsd command error")

type upsUnit struct {
    name string
    vars map[string]string
}

func newUpsdConn(conf Config) upsdConn {
    return &upsdClient{conn: socket.New(socket.Config{
        ConnectTimeout: conf.Timeout.Duration(),
        ReadTimeout:    conf.Timeout.Duration(),
        WriteTimeout:   conf.Timeout.Duration(),
        Address:        conf.Address,
    })}
}

type upsdClient struct {
    conn socket.Client
}

func (c *upsdClient) connect() error {
    return c.conn.Connect()
}

func (c *upsdClient) disconnect() error {
    _, _ = c.sendCommand(commandLogout)
    return c.conn.Disconnect()
}

func (c *upsdClient) authenticate(username, password string) error {
    cmd := fmt.Sprintf(commandUsername, username)
    resp, err := c.sendCommand(cmd)
    if err != nil {
        return err
    }
    if resp[0] != "OK" {
        return errors.New("authentication failed: invalid username")
    }

    cmd = fmt.Sprintf(commandPassword, password)
    resp, err = c.sendCommand(cmd)
    if err != nil {
        return err
    }
    if resp[0] != "OK" {
        return errors.New("authentication failed: invalid password")
    }

    return nil
}

func (c *upsdClient) upsUnits() ([]upsUnit, error) {
    resp, err := c.sendCommand(commandListUPS)
    if err != nil {
        return nil, err
    }

    var upsNames []string

    for _, v := range resp {
        if !strings.HasPrefix(v, "UPS ") {
            continue
        }
        parts := splitLine(v)
        if len(parts) < 2 {
            continue
        }
        name := parts[1]
        upsNames = append(upsNames, name)
    }

    var upsUnits []upsUnit

    for _, name := range upsNames {
        cmd := fmt.Sprintf(commandListVar, name)
        resp, err := c.sendCommand(cmd)
        if err != nil {
            return nil, err
        }

        ups := upsUnit{
            name: name,
            vars: make(map[string]string),
        }

        upsUnits = append(upsUnits, ups)

        for _, v := range resp {
            if !strings.HasPrefix(v, "VAR ") {
                continue
            }
            parts := splitLine(v)
            if len(parts) < 4 {
                continue
            }
            n, v := parts[2], parts[3]
            ups.vars[n] = v
        }
    }

    return upsUnits, nil
}

func (c *upsdClient) sendCommand(cmd string) ([]string, error) {
    var resp []string
    var errMsg string
    endLine := getEndLine(cmd)

    err := c.conn.Command(cmd+"\n", func(bytes []byte) bool {
        line := string(bytes)
        resp = append(resp, line)

        if strings.HasPrefix(line, "ERR ") {
            errMsg = strings.TrimPrefix(line, "ERR ")
        }

        return line != endLine && errMsg == ""
    })
    if err != nil {
        return nil, err
    }
    if errMsg != "" {
        return nil, fmt.Errorf("%w: %s (cmd: '%s')", errUpsdCommand, errMsg, cmd)
    }

    return resp, nil
}

func getEndLine(cmd string) string {
    px, _, _ := strings.Cut(cmd, " ")

    switch px {
    case "USERNAME", "PASSWORD", "VER":
        return "OK"
    }
    return fmt.Sprintf("END %s", cmd)
}

func splitLine(s string) []string {
    r := csv.NewReader(strings.NewReader(s))
    r.Comma = ' '

    parts, _ := r.Read()

    return parts
}