master
go 173 lines 3.24 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package upsd
4
5 import (
6 "encoding/csv"
7 "errors"
8 "fmt"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
12 )
13
14 const (
15 commandUsername = "USERNAME %s"
16 commandPassword = "PASSWORD %s"
17 commandListUPS = "LIST UPS"
18 commandListVar = "LIST VAR %s"
19 commandLogout = "LOGOUT"
20 )
21
22 // https://github.com/networkupstools/nut/blob/81fca30b2998fa73085ce4654f075605ff0b9e01/docs/net-protocol.txt#L647
23 var errUpsdCommand = errors.New("upsd command error")
24
25 type upsdConn interface {
26 connect() error
27 disconnect() error
28 authenticate(string, string) error
29 upsUnits() ([]upsUnit, error)
30 }
31
32 type upsUnit struct {
33 name string
34 vars map[string]string
35 }
36
37 func newUpsdConn(conf Config) upsdConn {
38 return &upsdClient{conn: socket.New(socket.Config{
39 Timeout: conf.Timeout.Duration(),
40 Address: conf.Address,
41 })}
42 }
43
44 type upsdClient struct {
45 conn socket.Client
46 }
47
48 func (c *upsdClient) connect() error {
49 return c.conn.Connect()
50 }
51
52 func (c *upsdClient) disconnect() error {
53 _, _ = c.sendCommand(commandLogout)
54 return c.conn.Disconnect()
55 }
56
57 func (c *upsdClient) authenticate(username, password string) error {
58 cmd := fmt.Sprintf(commandUsername, username)
59 resp, err := c.sendCommand(cmd)
60 if err != nil {
61 return err
62 }
63 if resp[0] != "OK" {
64 return errors.New("authentication failed: invalid username")
65 }
66
67 cmd = fmt.Sprintf(commandPassword, password)
68 resp, err = c.sendCommand(cmd)
69 if err != nil {
70 return err
71 }
72 if resp[0] != "OK" {
73 return errors.New("authentication failed: invalid password")
74 }
75
76 return nil
77 }
78
79 func (c *upsdClient) upsUnits() ([]upsUnit, error) {
80 resp, err := c.sendCommand(commandListUPS)
81 if err != nil {
82 return nil, err
83 }
84
85 var upsNames []string
86
87 for _, v := range resp {
88 if !strings.HasPrefix(v, "UPS ") {
89 continue
90 }
91 parts := splitLine(v)
92 if len(parts) < 2 {
93 continue
94 }
95 name := parts[1]
96 upsNames = append(upsNames, name)
97 }
98
99 var upsUnits []upsUnit
100
101 for _, name := range upsNames {
102 cmd := fmt.Sprintf(commandListVar, name)
103 resp, err := c.sendCommand(cmd)
104 if err != nil {
105 return nil, err
106 }
107
108 ups := upsUnit{
109 name: name,
110 vars: make(map[string]string),
111 }
112
113 upsUnits = append(upsUnits, ups)
114
115 for _, v := range resp {
116 if !strings.HasPrefix(v, "VAR ") {
117 continue
118 }
119 parts := splitLine(v)
120 if len(parts) < 4 {
121 continue
122 }
123 n, v := parts[2], parts[3]
124 ups.vars[n] = v
125 }
126 }
127
128 return upsUnits, nil
129 }
130
131 func (c *upsdClient) sendCommand(cmd string) ([]string, error) {
132 var resp []string
133 var errMsg string
134 endLine := getEndLine(cmd)
135
136 err := c.conn.Command(cmd+"\n", func(bytes []byte) (bool, error) {
137 line := string(bytes)
138 resp = append(resp, line)
139
140 if after, ok := strings.CutPrefix(line, "ERR "); ok {
141 errMsg = after
142 }
143
144 return line != endLine && errMsg == "", nil
145 })
146 if err != nil {
147 return nil, err
148 }
149 if errMsg != "" {
150 return nil, fmt.Errorf("%w: %s (cmd: '%s')", errUpsdCommand, errMsg, cmd)
151 }
152
153 return resp, nil
154 }
155
156 func getEndLine(cmd string) string {
157 px, _, _ := strings.Cut(cmd, " ")
158
159 switch px {
160 case "USERNAME", "PASSWORD", "VER":
161 return "OK"
162 }
163 return fmt.Sprintf("END %s", cmd)
164 }
165
166 func splitLine(s string) []string {
167 r := csv.NewReader(strings.NewReader(s))
168 r.Comma = ' '
169
170 parts, _ := r.Read()
171
172 return parts
173 }