1
1
package ipmi_sensor
2
2
3
3
import (
4
+ "bufio"
5
+ "bytes"
4
6
"fmt"
5
7
"os/exec"
8
+ "regexp"
6
9
"strconv"
7
10
"strings"
8
11
"sync"
@@ -14,14 +17,20 @@ import (
14
17
)
15
18
16
19
var (
17
- execCommand = exec .Command // execCommand is used to mock commands in tests.
20
+ execCommand = exec .Command // execCommand is used to mock commands in tests.
21
+ re_v1_parse_line = regexp .MustCompile (`^(?P<name>[^|]*)\|(?P<description>[^|]*)\|(?P<status_code>.*)` )
22
+ re_v2_parse_line = regexp .MustCompile (`^(?P<name>[^|]*)\|[^|]+\|(?P<status_code>[^|]*)\|(?P<entity_id>[^|]*)\|(?:(?P<description>[^|]+))?` )
23
+ re_v2_parse_description = regexp .MustCompile (`^(?P<analogValue>[0-9.]+)\s(?P<analogUnit>.*)|(?P<status>.+)|^$` )
24
+ re_v2_parse_unit = regexp .MustCompile (`^(?P<realAnalogUnit>[^,]+)(?:,\s*(?P<statusDesc>.*))?` )
18
25
)
19
26
27
+ // Ipmi stores the configuration values for the ipmi_sensor input plugin
20
28
type Ipmi struct {
21
- Path string
22
- Privilege string
23
- Servers []string
24
- Timeout internal.Duration
29
+ Path string
30
+ Privilege string
31
+ Servers []string
32
+ Timeout internal.Duration
33
+ MetricVersion int
25
34
}
26
35
27
36
var sampleConfig = `
@@ -46,16 +55,22 @@ var sampleConfig = `
46
55
47
56
## Timeout for the ipmitool command to complete
48
57
timeout = "20s"
58
+
59
+ ## Schema Version: (Optional, defaults to version 1)
60
+ metric_version = 2
49
61
`
50
62
63
+ // SampleConfig returns the documentation about the sample configuration
51
64
func (m * Ipmi ) SampleConfig () string {
52
65
return sampleConfig
53
66
}
54
67
68
+ // Description returns a basic description for the plugin functions
55
69
func (m * Ipmi ) Description () string {
56
70
return "Read metrics from the bare metal servers via IPMI"
57
71
}
58
72
73
+ // Gather is the main execution function for the plugin
59
74
func (m * Ipmi ) Gather (acc telegraf.Accumulator ) error {
60
75
if len (m .Path ) == 0 {
61
76
return fmt .Errorf ("ipmitool not found: verify that ipmitool is installed and that ipmitool is in your PATH" )
@@ -93,23 +108,33 @@ func (m *Ipmi) parse(acc telegraf.Accumulator, server string) error {
93
108
opts = conn .options ()
94
109
}
95
110
opts = append (opts , "sdr" )
111
+ if m .MetricVersion == 2 {
112
+ opts = append (opts , "elist" )
113
+ }
96
114
cmd := execCommand (m .Path , opts ... )
97
115
out , err := internal .CombinedOutputTimeout (cmd , m .Timeout .Duration )
116
+ timestamp := time .Now ()
98
117
if err != nil {
99
118
return fmt .Errorf ("failed to run command %s: %s - %s" , strings .Join (cmd .Args , " " ), err , string (out ))
100
119
}
120
+ if m .MetricVersion == 2 {
121
+ return parseV2 (acc , hostname , out , timestamp )
122
+ }
123
+ return parseV1 (acc , hostname , out , timestamp )
124
+ }
101
125
126
+ func parseV1 (acc telegraf.Accumulator , hostname string , cmdOut []byte , measured_at time.Time ) error {
102
127
// each line will look something like
103
128
// Planar VBAT | 3.05 Volts | ok
104
- lines := strings . Split ( string ( out ), " \n " )
105
- for i := 0 ; i < len ( lines ); i ++ {
106
- vals := strings . Split ( lines [ i ], "|" )
107
- if len (vals ) != 3 {
129
+ scanner := bufio . NewScanner ( bytes . NewReader ( cmdOut ) )
130
+ for scanner . Scan () {
131
+ ipmiFields := extractFieldsFromRegex ( re_v1_parse_line , scanner . Text () )
132
+ if len (ipmiFields ) != 3 {
108
133
continue
109
134
}
110
135
111
136
tags := map [string ]string {
112
- "name" : transform (vals [ 0 ]),
137
+ "name" : transform (ipmiFields [ "name" ]),
113
138
}
114
139
115
140
// tag the server is we have one
@@ -118,38 +143,106 @@ func (m *Ipmi) parse(acc telegraf.Accumulator, server string) error {
118
143
}
119
144
120
145
fields := make (map [string ]interface {})
121
- if strings .EqualFold ("ok" , trim (vals [ 2 ])) {
146
+ if strings .EqualFold ("ok" , trim (ipmiFields [ "status_code" ])) {
122
147
fields ["status" ] = 1
123
148
} else {
124
149
fields ["status" ] = 0
125
150
}
126
151
127
- val1 := trim (vals [1 ])
128
-
129
- if strings .Index (val1 , " " ) > 0 {
152
+ if strings .Index (ipmiFields ["description" ], " " ) > 0 {
130
153
// split middle column into value and unit
131
- valunit := strings .SplitN (val1 , " " , 2 )
132
- fields ["value" ] = Atofloat (valunit [0 ])
154
+ valunit := strings .SplitN (ipmiFields ["description" ], " " , 2 )
155
+ var err error
156
+ fields ["value" ], err = aToFloat (valunit [0 ])
157
+ if err != nil {
158
+ continue
159
+ }
133
160
if len (valunit ) > 1 {
134
161
tags ["unit" ] = transform (valunit [1 ])
135
162
}
136
163
} else {
137
164
fields ["value" ] = 0.0
138
165
}
139
166
140
- acc .AddFields ("ipmi_sensor" , fields , tags , time . Now () )
167
+ acc .AddFields ("ipmi_sensor" , fields , tags , measured_at )
141
168
}
142
169
143
- return nil
170
+ return scanner . Err ()
144
171
}
145
172
146
- func Atofloat (val string ) float64 {
173
+ func parseV2 (acc telegraf.Accumulator , hostname string , cmdOut []byte , measured_at time.Time ) error {
174
+ // each line will look something like
175
+ // CMOS Battery | 65h | ok | 7.1 |
176
+ // Temp | 0Eh | ok | 3.1 | 55 degrees C
177
+ // Drive 0 | A0h | ok | 7.1 | Drive Present
178
+ scanner := bufio .NewScanner (bytes .NewReader (cmdOut ))
179
+ for scanner .Scan () {
180
+ ipmiFields := extractFieldsFromRegex (re_v2_parse_line , scanner .Text ())
181
+ if len (ipmiFields ) < 3 || len (ipmiFields ) > 4 {
182
+ continue
183
+ }
184
+
185
+ tags := map [string ]string {
186
+ "name" : transform (ipmiFields ["name" ]),
187
+ }
188
+
189
+ // tag the server is we have one
190
+ if hostname != "" {
191
+ tags ["server" ] = hostname
192
+ }
193
+ tags ["entity_id" ] = transform (ipmiFields ["entity_id" ])
194
+ tags ["status_code" ] = trim (ipmiFields ["status_code" ])
195
+ fields := make (map [string ]interface {})
196
+ descriptionResults := extractFieldsFromRegex (re_v2_parse_description , trim (ipmiFields ["description" ]))
197
+ // This is an analog value with a unit
198
+ if descriptionResults ["analogValue" ] != "" && len (descriptionResults ["analogUnit" ]) >= 1 {
199
+ var err error
200
+ fields ["value" ], err = aToFloat (descriptionResults ["analogValue" ])
201
+ if err != nil {
202
+ continue
203
+ }
204
+ // Some implementations add an extra status to their analog units
205
+ unitResults := extractFieldsFromRegex (re_v2_parse_unit , descriptionResults ["analogUnit" ])
206
+ tags ["unit" ] = transform (unitResults ["realAnalogUnit" ])
207
+ if unitResults ["statusDesc" ] != "" {
208
+ tags ["status_desc" ] = transform (unitResults ["statusDesc" ])
209
+ }
210
+ } else {
211
+ // This is a status value
212
+ fields ["value" ] = 0.0
213
+ // Extended status descriptions aren't required, in which case for consistency re-use the status code
214
+ if descriptionResults ["status" ] != "" {
215
+ tags ["status_desc" ] = transform (descriptionResults ["status" ])
216
+ } else {
217
+ tags ["status_desc" ] = transform (ipmiFields ["status_code" ])
218
+ }
219
+ }
220
+
221
+ acc .AddFields ("ipmi_sensor" , fields , tags , measured_at )
222
+ }
223
+
224
+ return scanner .Err ()
225
+ }
226
+
227
+ // extractFieldsFromRegex consumes a regex with named capture groups and returns a kvp map of strings with the results
228
+ func extractFieldsFromRegex (re * regexp.Regexp , input string ) map [string ]string {
229
+ submatches := re .FindStringSubmatch (input )
230
+ results := make (map [string ]string )
231
+ for i , name := range re .SubexpNames () {
232
+ if name != input && name != "" && input != "" {
233
+ results [name ] = trim (submatches [i ])
234
+ }
235
+ }
236
+ return results
237
+ }
238
+
239
+ // aToFloat converts string representations of numbers to float64 values
240
+ func aToFloat (val string ) (float64 , error ) {
147
241
f , err := strconv .ParseFloat (val , 64 )
148
242
if err != nil {
149
- return 0.0
150
- } else {
151
- return f
243
+ return 0.0 , err
152
244
}
245
+ return f , nil
153
246
}
154
247
155
248
func trim (s string ) string {
0 commit comments