2016-03-23 15:40:38 +00:00
|
|
|
package ipmi_sensor
|
2016-03-17 15:45:29 +00:00
|
|
|
|
|
|
|
import (
|
2018-07-31 23:56:03 +00:00
|
|
|
"bufio"
|
|
|
|
"bytes"
|
2017-01-09 10:45:31 +00:00
|
|
|
"fmt"
|
|
|
|
"os/exec"
|
2018-07-31 23:56:03 +00:00
|
|
|
"regexp"
|
2016-03-17 15:45:29 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-07-03 02:06:57 +00:00
|
|
|
"sync"
|
2016-03-17 15:45:29 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/influxdata/telegraf"
|
2017-01-09 10:45:31 +00:00
|
|
|
"github.com/influxdata/telegraf/internal"
|
2016-03-17 15:45:29 +00:00
|
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
|
|
|
)
|
|
|
|
|
2017-01-09 10:45:31 +00:00
|
|
|
var (
|
2018-07-31 23:56:03 +00:00
|
|
|
execCommand = exec.Command // execCommand is used to mock commands in tests.
|
|
|
|
re_v1_parse_line = regexp.MustCompile(`^(?P<name>[^|]*)\|(?P<description>[^|]*)\|(?P<status_code>.*)`)
|
|
|
|
re_v2_parse_line = regexp.MustCompile(`^(?P<name>[^|]*)\|[^|]+\|(?P<status_code>[^|]*)\|(?P<entity_id>[^|]*)\|(?:(?P<description>[^|]+))?`)
|
|
|
|
re_v2_parse_description = regexp.MustCompile(`^(?P<analogValue>[0-9.]+)\s(?P<analogUnit>.*)|(?P<status>.+)|^$`)
|
|
|
|
re_v2_parse_unit = regexp.MustCompile(`^(?P<realAnalogUnit>[^,]+)(?:,\s*(?P<statusDesc>.*))?`)
|
2017-01-09 10:45:31 +00:00
|
|
|
)
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
// Ipmi stores the configuration values for the ipmi_sensor input plugin
|
2016-03-17 15:45:29 +00:00
|
|
|
type Ipmi struct {
|
2018-07-31 23:56:03 +00:00
|
|
|
Path string
|
|
|
|
Privilege string
|
|
|
|
Servers []string
|
|
|
|
Timeout internal.Duration
|
|
|
|
MetricVersion int
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var sampleConfig = `
|
2017-01-09 10:45:31 +00:00
|
|
|
## optionally specify the path to the ipmitool executable
|
|
|
|
# path = "/usr/bin/ipmitool"
|
2018-01-05 23:59:25 +00:00
|
|
|
##
|
|
|
|
## optionally force session privilege level. Can be CALLBACK, USER, OPERATOR, ADMINISTRATOR
|
|
|
|
# privilege = "ADMINISTRATOR"
|
|
|
|
##
|
2017-01-09 10:45:31 +00:00
|
|
|
## optionally specify one or more servers via a url matching
|
2016-03-17 15:45:29 +00:00
|
|
|
## [username[:password]@][protocol[(address)]]
|
|
|
|
## e.g.
|
|
|
|
## root:passwd@lan(127.0.0.1)
|
|
|
|
##
|
2017-01-09 10:45:31 +00:00
|
|
|
## if no servers are specified, local machine sensor stats will be queried
|
|
|
|
##
|
|
|
|
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
2017-05-22 20:41:34 +00:00
|
|
|
|
2017-11-01 00:00:06 +00:00
|
|
|
## Recommended: use metric 'interval' that is a multiple of 'timeout' to avoid
|
2017-05-22 20:41:34 +00:00
|
|
|
## gaps or overlap in pulled data
|
|
|
|
interval = "30s"
|
|
|
|
|
|
|
|
## Timeout for the ipmitool command to complete
|
|
|
|
timeout = "20s"
|
2018-07-31 23:56:03 +00:00
|
|
|
|
|
|
|
## Schema Version: (Optional, defaults to version 1)
|
|
|
|
metric_version = 2
|
2016-03-17 15:45:29 +00:00
|
|
|
`
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
// SampleConfig returns the documentation about the sample configuration
|
2016-03-17 15:45:29 +00:00
|
|
|
func (m *Ipmi) SampleConfig() string {
|
|
|
|
return sampleConfig
|
|
|
|
}
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
// Description returns a basic description for the plugin functions
|
2016-03-17 15:45:29 +00:00
|
|
|
func (m *Ipmi) Description() string {
|
2017-01-09 10:45:31 +00:00
|
|
|
return "Read metrics from the bare metal servers via IPMI"
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
// Gather is the main execution function for the plugin
|
2016-03-17 15:45:29 +00:00
|
|
|
func (m *Ipmi) Gather(acc telegraf.Accumulator) error {
|
2017-03-08 16:38:36 +00:00
|
|
|
if len(m.Path) == 0 {
|
2017-01-09 10:45:31 +00:00
|
|
|
return fmt.Errorf("ipmitool not found: verify that ipmitool is installed and that ipmitool is in your PATH")
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
2017-01-09 10:45:31 +00:00
|
|
|
|
|
|
|
if len(m.Servers) > 0 {
|
2018-07-03 02:06:57 +00:00
|
|
|
wg := sync.WaitGroup{}
|
2017-01-09 10:45:31 +00:00
|
|
|
for _, server := range m.Servers {
|
2018-07-03 02:06:57 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func(a telegraf.Accumulator, s string) {
|
|
|
|
defer wg.Done()
|
|
|
|
err := m.parse(a, s)
|
|
|
|
if err != nil {
|
|
|
|
a.AddError(err)
|
|
|
|
}
|
|
|
|
}(acc, server)
|
2017-01-09 10:45:31 +00:00
|
|
|
}
|
2018-07-03 02:06:57 +00:00
|
|
|
wg.Wait()
|
2017-01-09 10:45:31 +00:00
|
|
|
} else {
|
|
|
|
err := m.parse(acc, "")
|
2016-03-17 15:45:29 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-01-09 10:45:31 +00:00
|
|
|
func (m *Ipmi) parse(acc telegraf.Accumulator, server string) error {
|
|
|
|
opts := make([]string, 0)
|
|
|
|
hostname := ""
|
|
|
|
if server != "" {
|
2018-01-05 23:59:25 +00:00
|
|
|
conn := NewConnection(server, m.Privilege)
|
2017-01-09 10:45:31 +00:00
|
|
|
hostname = conn.Hostname
|
|
|
|
opts = conn.options()
|
|
|
|
}
|
|
|
|
opts = append(opts, "sdr")
|
2018-07-31 23:56:03 +00:00
|
|
|
if m.MetricVersion == 2 {
|
|
|
|
opts = append(opts, "elist")
|
|
|
|
}
|
2017-03-08 16:38:36 +00:00
|
|
|
cmd := execCommand(m.Path, opts...)
|
2017-05-22 20:41:34 +00:00
|
|
|
out, err := internal.CombinedOutputTimeout(cmd, m.Timeout.Duration)
|
2018-07-31 23:56:03 +00:00
|
|
|
timestamp := time.Now()
|
2016-03-17 15:45:29 +00:00
|
|
|
if err != nil {
|
2017-01-09 10:45:31 +00:00
|
|
|
return fmt.Errorf("failed to run command %s: %s - %s", strings.Join(cmd.Args, " "), err, string(out))
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
2018-07-31 23:56:03 +00:00
|
|
|
if m.MetricVersion == 2 {
|
|
|
|
return parseV2(acc, hostname, out, timestamp)
|
|
|
|
}
|
|
|
|
return parseV1(acc, hostname, out, timestamp)
|
|
|
|
}
|
2016-03-17 15:45:29 +00:00
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
func parseV1(acc telegraf.Accumulator, hostname string, cmdOut []byte, measured_at time.Time) error {
|
2016-03-23 15:40:38 +00:00
|
|
|
// each line will look something like
|
|
|
|
// Planar VBAT | 3.05 Volts | ok
|
2018-07-31 23:56:03 +00:00
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(cmdOut))
|
|
|
|
for scanner.Scan() {
|
|
|
|
ipmiFields := extractFieldsFromRegex(re_v1_parse_line, scanner.Text())
|
|
|
|
if len(ipmiFields) != 3 {
|
2016-03-23 15:40:38 +00:00
|
|
|
continue
|
|
|
|
}
|
2016-03-17 15:45:29 +00:00
|
|
|
|
2016-03-23 15:40:38 +00:00
|
|
|
tags := map[string]string{
|
2018-07-31 23:56:03 +00:00
|
|
|
"name": transform(ipmiFields["name"]),
|
2017-01-09 10:45:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// tag the server is we have one
|
|
|
|
if hostname != "" {
|
|
|
|
tags["server"] = hostname
|
2016-03-23 15:40:38 +00:00
|
|
|
}
|
2016-03-17 15:45:29 +00:00
|
|
|
|
2016-03-23 15:40:38 +00:00
|
|
|
fields := make(map[string]interface{})
|
2018-07-31 23:56:03 +00:00
|
|
|
if strings.EqualFold("ok", trim(ipmiFields["status_code"])) {
|
2016-03-23 15:40:38 +00:00
|
|
|
fields["status"] = 1
|
|
|
|
} else {
|
|
|
|
fields["status"] = 0
|
|
|
|
}
|
2016-03-17 15:45:29 +00:00
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
if strings.Index(ipmiFields["description"], " ") > 0 {
|
2016-03-23 15:40:38 +00:00
|
|
|
// split middle column into value and unit
|
2018-07-31 23:56:03 +00:00
|
|
|
valunit := strings.SplitN(ipmiFields["description"], " ", 2)
|
|
|
|
var err error
|
|
|
|
fields["value"], err = aToFloat(valunit[0])
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2016-03-23 15:40:38 +00:00
|
|
|
if len(valunit) > 1 {
|
|
|
|
tags["unit"] = transform(valunit[1])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fields["value"] = 0.0
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
2016-03-23 15:40:38 +00:00
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
acc.AddFields("ipmi_sensor", fields, tags, measured_at)
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
return scanner.Err()
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
|
|
|
|
2018-07-31 23:56:03 +00:00
|
|
|
func parseV2(acc telegraf.Accumulator, hostname string, cmdOut []byte, measured_at time.Time) error {
|
|
|
|
// each line will look something like
|
|
|
|
// CMOS Battery | 65h | ok | 7.1 |
|
|
|
|
// Temp | 0Eh | ok | 3.1 | 55 degrees C
|
|
|
|
// Drive 0 | A0h | ok | 7.1 | Drive Present
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(cmdOut))
|
|
|
|
for scanner.Scan() {
|
|
|
|
ipmiFields := extractFieldsFromRegex(re_v2_parse_line, scanner.Text())
|
|
|
|
if len(ipmiFields) < 3 || len(ipmiFields) > 4 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
tags := map[string]string{
|
|
|
|
"name": transform(ipmiFields["name"]),
|
|
|
|
}
|
|
|
|
|
|
|
|
// tag the server is we have one
|
|
|
|
if hostname != "" {
|
|
|
|
tags["server"] = hostname
|
|
|
|
}
|
|
|
|
tags["entity_id"] = transform(ipmiFields["entity_id"])
|
|
|
|
tags["status_code"] = trim(ipmiFields["status_code"])
|
|
|
|
fields := make(map[string]interface{})
|
|
|
|
descriptionResults := extractFieldsFromRegex(re_v2_parse_description, trim(ipmiFields["description"]))
|
|
|
|
// This is an analog value with a unit
|
|
|
|
if descriptionResults["analogValue"] != "" && len(descriptionResults["analogUnit"]) >= 1 {
|
|
|
|
var err error
|
|
|
|
fields["value"], err = aToFloat(descriptionResults["analogValue"])
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Some implementations add an extra status to their analog units
|
|
|
|
unitResults := extractFieldsFromRegex(re_v2_parse_unit, descriptionResults["analogUnit"])
|
|
|
|
tags["unit"] = transform(unitResults["realAnalogUnit"])
|
|
|
|
if unitResults["statusDesc"] != "" {
|
|
|
|
tags["status_desc"] = transform(unitResults["statusDesc"])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This is a status value
|
|
|
|
fields["value"] = 0.0
|
|
|
|
// Extended status descriptions aren't required, in which case for consistency re-use the status code
|
|
|
|
if descriptionResults["status"] != "" {
|
|
|
|
tags["status_desc"] = transform(descriptionResults["status"])
|
|
|
|
} else {
|
|
|
|
tags["status_desc"] = transform(ipmiFields["status_code"])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
acc.AddFields("ipmi_sensor", fields, tags, measured_at)
|
|
|
|
}
|
|
|
|
|
|
|
|
return scanner.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
// extractFieldsFromRegex consumes a regex with named capture groups and returns a kvp map of strings with the results
|
|
|
|
func extractFieldsFromRegex(re *regexp.Regexp, input string) map[string]string {
|
|
|
|
submatches := re.FindStringSubmatch(input)
|
|
|
|
results := make(map[string]string)
|
|
|
|
for i, name := range re.SubexpNames() {
|
|
|
|
if name != input && name != "" && input != "" {
|
|
|
|
results[name] = trim(submatches[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results
|
|
|
|
}
|
|
|
|
|
|
|
|
// aToFloat converts string representations of numbers to float64 values
|
|
|
|
func aToFloat(val string) (float64, error) {
|
2016-03-17 15:45:29 +00:00
|
|
|
f, err := strconv.ParseFloat(val, 64)
|
|
|
|
if err != nil {
|
2018-07-31 23:56:03 +00:00
|
|
|
return 0.0, err
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
2018-07-31 23:56:03 +00:00
|
|
|
return f, nil
|
2016-03-17 15:45:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func trim(s string) string {
|
|
|
|
return strings.TrimSpace(s)
|
|
|
|
}
|
|
|
|
|
2016-03-23 15:40:38 +00:00
|
|
|
func transform(s string) string {
|
|
|
|
s = trim(s)
|
|
|
|
s = strings.ToLower(s)
|
|
|
|
return strings.Replace(s, " ", "_", -1)
|
|
|
|
}
|
|
|
|
|
2016-03-17 15:45:29 +00:00
|
|
|
func init() {
|
2017-01-09 10:45:31 +00:00
|
|
|
m := Ipmi{}
|
|
|
|
path, _ := exec.LookPath("ipmitool")
|
|
|
|
if len(path) > 0 {
|
2017-03-08 16:38:36 +00:00
|
|
|
m.Path = path
|
2017-01-09 10:45:31 +00:00
|
|
|
}
|
2017-05-22 20:41:34 +00:00
|
|
|
m.Timeout = internal.Duration{Duration: time.Second * 20}
|
2016-03-23 15:40:38 +00:00
|
|
|
inputs.Add("ipmi_sensor", func() telegraf.Input {
|
2017-04-20 00:02:44 +00:00
|
|
|
m := m
|
2017-01-09 10:45:31 +00:00
|
|
|
return &m
|
2016-03-17 15:45:29 +00:00
|
|
|
})
|
|
|
|
}
|