Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
213
plugins/inputs/ipmi_sensor/README.md
Normal file
213
plugins/inputs/ipmi_sensor/README.md
Normal file
|
@ -0,0 +1,213 @@
|
|||
# IPMI Sensor Input Plugin
|
||||
|
||||
This plugin gathers metrics from the
|
||||
[Intelligent Platform Management Interface][ipmi_spec] using the
|
||||
[`ipmitool`][ipmitool] command line utility.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `ipmitool` requires access to the IPMI device. Please check the
|
||||
> [permission section](#permissions) for possible solutions.
|
||||
|
||||
⭐ Telegraf v0.12.0
|
||||
🏷️ hardware, system
|
||||
💻 all
|
||||
|
||||
[ipmi_spec]: https://www.intel.com/content/dam/www/public/us/en/documents/specification-updates/ipmi-intelligent-platform-mgt-interface-spec-2nd-gen-v2-0-spec-update.pdf
|
||||
[ipmitool]: https://github.com/ipmitool/ipmitool
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Read metrics from the bare metal servers via IPMI
|
||||
[[inputs.ipmi_sensor]]
|
||||
## Specify the path to the ipmitool executable
|
||||
# path = "/usr/bin/ipmitool"
|
||||
|
||||
## Use sudo
|
||||
## Setting 'use_sudo' to true will make use of sudo to run ipmitool.
|
||||
## Sudo must be configured to allow the telegraf user to run ipmitool
|
||||
## without a password.
|
||||
# use_sudo = false
|
||||
|
||||
## Servers
|
||||
## Specify one or more servers via a url. If no servers are specified, local
|
||||
## machine sensor stats will be queried. Uses the format:
|
||||
## [username[:password]@][protocol[(address)]]
|
||||
## e.g. root:passwd@lan(127.0.0.1)
|
||||
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
||||
|
||||
## Session privilege level
|
||||
## Choose from: CALLBACK, USER, OPERATOR, ADMINISTRATOR
|
||||
# privilege = "ADMINISTRATOR"
|
||||
|
||||
## Timeout
|
||||
## Timeout for the ipmitool command to complete.
|
||||
# timeout = "20s"
|
||||
|
||||
## Metric schema version
|
||||
## See the plugin readme for more information on schema versioning.
|
||||
# metric_version = 1
|
||||
|
||||
## Sensors to collect
|
||||
## Choose from:
|
||||
## * sdr: default, collects sensor data records
|
||||
## * chassis_power_status: collects the power status of the chassis
|
||||
## * dcmi_power_reading: collects the power readings from the Data Center Management Interface
|
||||
# sensors = ["sdr"]
|
||||
|
||||
## Hex key
|
||||
## Optionally provide the hex key for the IMPI connection.
|
||||
# hex_key = ""
|
||||
|
||||
## Cache
|
||||
## If ipmitool should use a cache
|
||||
## Using a cache can speed up collection times depending on your device.
|
||||
# use_cache = false
|
||||
|
||||
## Path to the ipmitools cache file (defaults to OS temp dir)
|
||||
## The provided path must exist and must be writable
|
||||
# cache_path = ""
|
||||
```
|
||||
|
||||
If no servers are specified, the plugin will query the local machine sensor
|
||||
stats via the following command:
|
||||
|
||||
```sh
|
||||
ipmitool sdr
|
||||
```
|
||||
|
||||
or with the version 2 schema:
|
||||
|
||||
```sh
|
||||
ipmitool sdr elist
|
||||
```
|
||||
|
||||
When one or more servers are specified, the plugin will use the following
|
||||
command to collect remote host sensor stats:
|
||||
|
||||
```sh
|
||||
ipmitool -I lan -H SERVER -U USERID -P PASSW0RD sdr
|
||||
```
|
||||
|
||||
Any of the following parameters will be added to the aforementioned query if
|
||||
they're configured:
|
||||
|
||||
```sh
|
||||
-y hex_key -L privilege
|
||||
```
|
||||
|
||||
## Sensors
|
||||
|
||||
By default the plugin collects data via the `sdr` command and returns those
|
||||
values. However, there are additonal sensor options that be call on:
|
||||
|
||||
- `chassis_power_status` - returns 0 or 1 depending on the output of
|
||||
`chassis power status`
|
||||
- `dcmi_power_reading` - Returns the watt values from `dcmi power reading`
|
||||
|
||||
These sensor options are not affected by the metric version.
|
||||
|
||||
## Metrics
|
||||
|
||||
Version 1 schema:
|
||||
|
||||
- ipmi_sensor:
|
||||
- tags:
|
||||
- name
|
||||
- unit
|
||||
- host
|
||||
- server (only when retrieving stats from remote servers)
|
||||
- fields:
|
||||
- status (int, 1=ok status_code/0=anything else)
|
||||
- value (float)
|
||||
|
||||
Version 2 schema:
|
||||
|
||||
- ipmi_sensor:
|
||||
- tags:
|
||||
- name
|
||||
- entity_id (can help uniquify duplicate names)
|
||||
- status_code (two letter code from IPMI documentation)
|
||||
- status_desc (extended status description field)
|
||||
- unit (only on analog values)
|
||||
- host
|
||||
- server (only when retrieving stats from remote)
|
||||
- fields:
|
||||
- value (float)
|
||||
|
||||
### Permissions
|
||||
|
||||
When gathering from the local system, Telegraf will need permission to the
|
||||
ipmi device node. When using udev you can create the device node giving
|
||||
`rw` permissions to the `telegraf` user by adding the following rule to
|
||||
`/etc/udev/rules.d/52-telegraf-ipmi.rules`:
|
||||
|
||||
```sh
|
||||
KERNEL=="ipmi*", MODE="660", GROUP="telegraf"
|
||||
```
|
||||
|
||||
Alternatively, it is possible to use sudo. You will need the following in your
|
||||
telegraf config:
|
||||
|
||||
```toml
|
||||
[[inputs.ipmi_sensor]]
|
||||
use_sudo = true
|
||||
```
|
||||
|
||||
You will also need to update your sudoers file:
|
||||
|
||||
```bash
|
||||
$ visudo
|
||||
# Add the following line:
|
||||
Cmnd_Alias IPMITOOL = /usr/bin/ipmitool *
|
||||
telegraf ALL=(root) NOPASSWD: IPMITOOL
|
||||
Defaults!IPMITOOL !logfile, !syslog, !pam_session
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
### Version 1 Schema
|
||||
|
||||
When retrieving stats from a remote server:
|
||||
|
||||
```text
|
||||
ipmi_sensor,server=10.20.2.203,name=uid_light value=0,status=1i 1517125513000000000
|
||||
ipmi_sensor,server=10.20.2.203,name=sys._health_led status=1i,value=0 1517125513000000000
|
||||
ipmi_sensor,server=10.20.2.203,name=power_supply_1,unit=watts status=1i,value=110 1517125513000000000
|
||||
ipmi_sensor,server=10.20.2.203,name=power_supply_2,unit=watts status=1i,value=120 1517125513000000000
|
||||
ipmi_sensor,server=10.20.2.203,name=power_supplies value=0,status=1i 1517125513000000000
|
||||
ipmi_sensor,server=10.20.2.203,name=fan_1,unit=percent status=1i,value=43.12 1517125513000000000
|
||||
```
|
||||
|
||||
When retrieving stats from the local machine (no server specified):
|
||||
|
||||
```text
|
||||
ipmi_sensor,name=uid_light value=0,status=1i 1517125513000000000
|
||||
ipmi_sensor,name=sys._health_led status=1i,value=0 1517125513000000000
|
||||
ipmi_sensor,name=power_supply_1,unit=watts status=1i,value=110 1517125513000000000
|
||||
ipmi_sensor,name=power_supply_2,unit=watts status=1i,value=120 1517125513000000000
|
||||
ipmi_sensor,name=power_supplies value=0,status=1i 1517125513000000000
|
||||
ipmi_sensor,name=fan_1,unit=percent status=1i,value=43.12 1517125513000000000
|
||||
```
|
||||
|
||||
#### Version 2 Schema
|
||||
|
||||
When retrieving stats from the local machine (no server specified):
|
||||
|
||||
```text
|
||||
ipmi_sensor,name=uid_light,entity_id=23.1,status_code=ok,status_desc=ok value=0 1517125474000000000
|
||||
ipmi_sensor,name=sys._health_led,entity_id=23.2,status_code=ok,status_desc=ok value=0 1517125474000000000
|
||||
ipmi_sensor,entity_id=10.1,name=power_supply_1,status_code=ok,status_desc=presence_detected,unit=watts value=110 1517125474000000000
|
||||
ipmi_sensor,name=power_supply_2,entity_id=10.2,status_code=ok,unit=watts,status_desc=presence_detected value=125 1517125474000000000
|
||||
ipmi_sensor,name=power_supplies,entity_id=10.3,status_code=ok,status_desc=fully_redundant value=0 1517125474000000000
|
||||
ipmi_sensor,entity_id=7.1,name=fan_1,status_code=ok,status_desc=transition_to_running,unit=percent value=43.12 1517125474000000000
|
||||
```
|
73
plugins/inputs/ipmi_sensor/connection.go
Normal file
73
plugins/inputs/ipmi_sensor/connection.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// connection properties for a Client
|
||||
type connection struct {
|
||||
hostname string
|
||||
username string
|
||||
password string
|
||||
port int
|
||||
intf string
|
||||
privilege string
|
||||
hexKey string
|
||||
}
|
||||
|
||||
func newConnection(server, privilege, hexKey string) *connection {
|
||||
conn := &connection{
|
||||
privilege: privilege,
|
||||
hexKey: hexKey,
|
||||
}
|
||||
inx1 := strings.LastIndex(server, "@")
|
||||
inx2 := strings.Index(server, "(")
|
||||
|
||||
connstr := server
|
||||
|
||||
if inx1 > 0 {
|
||||
security := server[0:inx1]
|
||||
connstr = server[inx1+1:]
|
||||
up := strings.SplitN(security, ":", 2)
|
||||
if len(up) == 2 {
|
||||
conn.username = up[0]
|
||||
conn.password = up[1]
|
||||
}
|
||||
}
|
||||
|
||||
if inx2 > 0 {
|
||||
inx2 = strings.Index(connstr, "(")
|
||||
inx3 := strings.Index(connstr, ")")
|
||||
|
||||
conn.intf = connstr[0:inx2]
|
||||
conn.hostname = connstr[inx2+1 : inx3]
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c *connection) options() []string {
|
||||
intf := c.intf
|
||||
if intf == "" {
|
||||
intf = "lan"
|
||||
}
|
||||
|
||||
options := []string{
|
||||
"-H", c.hostname,
|
||||
"-U", c.username,
|
||||
"-P", c.password,
|
||||
"-I", intf,
|
||||
}
|
||||
|
||||
if c.hexKey != "" {
|
||||
options = append(options, "-y", c.hexKey)
|
||||
}
|
||||
if c.port != 0 {
|
||||
options = append(options, "-p", strconv.Itoa(c.port))
|
||||
}
|
||||
if c.privilege != "" {
|
||||
options = append(options, "-L", c.privilege)
|
||||
}
|
||||
return options
|
||||
}
|
87
plugins/inputs/ipmi_sensor/connection_test.go
Normal file
87
plugins/inputs/ipmi_sensor/connection_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewConnection(t *testing.T) {
|
||||
testData := []struct {
|
||||
addr string
|
||||
con *connection
|
||||
}{
|
||||
{
|
||||
"USERID:PASSW0RD@lan(192.168.1.1)",
|
||||
&connection{
|
||||
hostname: "192.168.1.1",
|
||||
username: "USERID",
|
||||
password: "PASSW0RD",
|
||||
intf: "lan",
|
||||
privilege: "USER",
|
||||
hexKey: "0001",
|
||||
},
|
||||
},
|
||||
{
|
||||
"USERID:PASS:!@#$%^&*(234)_+W0RD@lan(192.168.1.1)",
|
||||
&connection{
|
||||
hostname: "192.168.1.1",
|
||||
username: "USERID",
|
||||
password: "PASS:!@#$%^&*(234)_+W0RD",
|
||||
intf: "lan",
|
||||
privilege: "USER",
|
||||
hexKey: "0001",
|
||||
},
|
||||
},
|
||||
// test connection doesn't panic if incorrect symbol used
|
||||
{
|
||||
"USERID@PASSW0RD@lan(192.168.1.1)",
|
||||
&connection{
|
||||
hostname: "192.168.1.1",
|
||||
username: "",
|
||||
password: "",
|
||||
intf: "lan",
|
||||
privilege: "USER",
|
||||
hexKey: "0001",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range testData {
|
||||
require.EqualValues(t, v.con, newConnection(v.addr, "USER", "0001"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommandOptions(t *testing.T) {
|
||||
testData := []struct {
|
||||
connection *connection
|
||||
options []string
|
||||
}{
|
||||
{
|
||||
&connection{
|
||||
hostname: "192.168.1.1",
|
||||
username: "user",
|
||||
password: "password",
|
||||
intf: "lan",
|
||||
privilege: "USER",
|
||||
hexKey: "0001",
|
||||
},
|
||||
[]string{"-H", "192.168.1.1", "-U", "user", "-P", "password", "-I", "lan", "-y", "0001", "-L", "USER"},
|
||||
},
|
||||
{
|
||||
&connection{
|
||||
hostname: "192.168.1.1",
|
||||
username: "user",
|
||||
password: "password",
|
||||
intf: "lan",
|
||||
privilege: "USER",
|
||||
hexKey: "",
|
||||
},
|
||||
[]string{"-H", "192.168.1.1", "-U", "user", "-P", "password", "-I", "lan", "-L", "USER"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
require.EqualValues(t, data.options, data.connection.options())
|
||||
}
|
||||
}
|
407
plugins/inputs/ipmi_sensor/ipmi_sensor.go
Normal file
407
plugins/inputs/ipmi_sensor/ipmi_sensor.go
Normal file
|
@ -0,0 +1,407 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/internal/choice"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
execCommand = exec.Command // execCommand is used to mock commands in tests.
|
||||
reV1ParseLine = regexp.MustCompile(`^(?P<name>[^|]*)\|(?P<description>[^|]*)\|(?P<status_code>.*)`)
|
||||
reV2ParseLine = regexp.MustCompile(`^(?P<name>[^|]*)\|[^|]+\|(?P<status_code>[^|]*)\|(?P<entity_id>[^|]*)\|(?:(?P<description>[^|]+))?`)
|
||||
reV2ParseDescription = regexp.MustCompile(`^(?P<analogValue>-?[0-9.]+)\s(?P<analogUnit>.*)|(?P<status>.+)|^$`)
|
||||
reV2ParseUnit = regexp.MustCompile(`^(?P<realAnalogUnit>[^,]+)(?:,\s*(?P<statusDesc>.*))?`)
|
||||
dcmiPowerReading = regexp.MustCompile(`^(?P<name>[^|]*)\:(?P<value>.* Watts)?`)
|
||||
)
|
||||
|
||||
const cmd = "ipmitool"
|
||||
|
||||
type Ipmi struct {
|
||||
Path string `toml:"path"`
|
||||
Privilege string `toml:"privilege"`
|
||||
HexKey string `toml:"hex_key"`
|
||||
Servers []string `toml:"servers"`
|
||||
Sensors []string `toml:"sensors"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
MetricVersion int `toml:"metric_version"`
|
||||
UseSudo bool `toml:"use_sudo"`
|
||||
UseCache bool `toml:"use_cache"`
|
||||
CachePath string `toml:"cache_path"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
}
|
||||
|
||||
func (*Ipmi) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (m *Ipmi) Init() error {
|
||||
// Set defaults
|
||||
if m.Path == "" {
|
||||
path, err := exec.LookPath(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("looking up %q failed: %w", cmd, err)
|
||||
}
|
||||
m.Path = path
|
||||
}
|
||||
if m.CachePath == "" {
|
||||
m.CachePath = os.TempDir()
|
||||
}
|
||||
if len(m.Sensors) == 0 {
|
||||
m.Sensors = []string{"sdr"}
|
||||
}
|
||||
if err := choice.CheckSlice(m.Sensors, []string{"sdr", "chassis_power_status", "dcmi_power_reading"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check parameters
|
||||
if m.Path == "" {
|
||||
return fmt.Errorf("no path for %q specified", cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Ipmi) Gather(acc telegraf.Accumulator) error {
|
||||
if len(m.Path) == 0 {
|
||||
return errors.New("ipmitool not found: verify that ipmitool is installed and that ipmitool is in your PATH")
|
||||
}
|
||||
|
||||
if len(m.Servers) > 0 {
|
||||
wg := sync.WaitGroup{}
|
||||
for _, server := range m.Servers {
|
||||
wg.Add(1)
|
||||
go func(a telegraf.Accumulator, s string) {
|
||||
defer wg.Done()
|
||||
for _, sensor := range m.Sensors {
|
||||
a.AddError(m.parse(a, s, sensor))
|
||||
}
|
||||
}(acc, server)
|
||||
}
|
||||
wg.Wait()
|
||||
} else {
|
||||
for _, sensor := range m.Sensors {
|
||||
err := m.parse(acc, "", sensor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Ipmi) parse(acc telegraf.Accumulator, server, sensor string) error {
|
||||
var command []string
|
||||
switch sensor {
|
||||
case "sdr":
|
||||
command = append(command, "sdr")
|
||||
case "chassis_power_status":
|
||||
command = append(command, "chassis", "power", "status")
|
||||
case "dcmi_power_reading":
|
||||
command = append(command, "dcmi", "power", "reading")
|
||||
default:
|
||||
return fmt.Errorf("unknown sensor type %q", sensor)
|
||||
}
|
||||
|
||||
opts := make([]string, 0)
|
||||
hostname := ""
|
||||
if server != "" {
|
||||
conn := newConnection(server, m.Privilege, m.HexKey)
|
||||
hostname = conn.hostname
|
||||
opts = conn.options()
|
||||
}
|
||||
|
||||
opts = append(opts, command...)
|
||||
|
||||
if m.UseCache {
|
||||
cacheFile := filepath.Join(m.CachePath, server+"_ipmi_cache")
|
||||
_, err := os.Stat(cacheFile)
|
||||
if os.IsNotExist(err) {
|
||||
dumpOpts := opts
|
||||
// init cache file
|
||||
dumpOpts = append(dumpOpts, "dump", cacheFile)
|
||||
name := m.Path
|
||||
if m.UseSudo {
|
||||
// -n - avoid prompting the user for input of any kind
|
||||
dumpOpts = append([]string{"-n", name}, dumpOpts...)
|
||||
name = "sudo"
|
||||
}
|
||||
cmd := execCommand(name, dumpOpts...)
|
||||
out, err := internal.CombinedOutputTimeout(cmd, time.Duration(m.Timeout))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run command %q: %w - %s", strings.Join(sanitizeIPMICmd(cmd.Args), " "), err, string(out))
|
||||
}
|
||||
}
|
||||
opts = append(opts, "-S", cacheFile)
|
||||
}
|
||||
if m.MetricVersion == 2 && sensor == "sdr" {
|
||||
opts = append(opts, "elist")
|
||||
}
|
||||
name := m.Path
|
||||
if m.UseSudo {
|
||||
// -n - avoid prompting the user for input of any kind
|
||||
opts = append([]string{"-n", name}, opts...)
|
||||
name = "sudo"
|
||||
}
|
||||
cmd := execCommand(name, opts...)
|
||||
out, err := internal.CombinedOutputTimeout(cmd, time.Duration(m.Timeout))
|
||||
timestamp := time.Now()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run command %q: %w - %s", strings.Join(sanitizeIPMICmd(cmd.Args), " "), err, string(out))
|
||||
}
|
||||
|
||||
switch sensor {
|
||||
case "sdr":
|
||||
if m.MetricVersion == 2 {
|
||||
return m.parseV2(acc, hostname, out, timestamp)
|
||||
}
|
||||
return m.parseV1(acc, hostname, out, timestamp)
|
||||
case "chassis_power_status":
|
||||
return parseChassisPowerStatus(acc, hostname, out, timestamp)
|
||||
case "dcmi_power_reading":
|
||||
return m.parseDCMIPowerReading(acc, hostname, out, timestamp)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown sensor type %q", sensor)
|
||||
}
|
||||
|
||||
func parseChassisPowerStatus(acc telegraf.Accumulator, hostname string, cmdOut []byte, measuredAt time.Time) error {
|
||||
// each line will look something like
|
||||
// Chassis Power is on
|
||||
// Chassis Power is off
|
||||
scanner := bufio.NewScanner(bytes.NewReader(cmdOut))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "Chassis Power is on") {
|
||||
acc.AddFields("ipmi_sensor", map[string]interface{}{"value": 1}, map[string]string{"name": "chassis_power_status", "server": hostname}, measuredAt)
|
||||
} else if strings.Contains(line, "Chassis Power is off") {
|
||||
acc.AddFields("ipmi_sensor", map[string]interface{}{"value": 0}, map[string]string{"name": "chassis_power_status", "server": hostname}, measuredAt)
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (m *Ipmi) parseDCMIPowerReading(acc telegraf.Accumulator, hostname string, cmdOut []byte, measuredAt time.Time) error {
|
||||
// each line will look something like
|
||||
// Current Power Reading : 0.000
|
||||
scanner := bufio.NewScanner(bytes.NewReader(cmdOut))
|
||||
for scanner.Scan() {
|
||||
ipmiFields := m.extractFieldsFromRegex(dcmiPowerReading, scanner.Text())
|
||||
if len(ipmiFields) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"name": transform(ipmiFields["name"]),
|
||||
}
|
||||
|
||||
// tag the server is we have one
|
||||
if hostname != "" {
|
||||
tags["server"] = hostname
|
||||
}
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
valunit := strings.Split(ipmiFields["value"], " ")
|
||||
if len(valunit) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
fields["value"], err = aToFloat(valunit[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(valunit) > 1 {
|
||||
tags["unit"] = transform(valunit[1])
|
||||
}
|
||||
|
||||
acc.AddFields("ipmi_sensor", fields, tags, measuredAt)
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (m *Ipmi) parseV1(acc telegraf.Accumulator, hostname string, cmdOut []byte, measuredAt time.Time) error {
|
||||
// each line will look something like
|
||||
// Planar VBAT | 3.05 Volts | ok
|
||||
scanner := bufio.NewScanner(bytes.NewReader(cmdOut))
|
||||
for scanner.Scan() {
|
||||
ipmiFields := m.extractFieldsFromRegex(reV1ParseLine, scanner.Text())
|
||||
if len(ipmiFields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"name": transform(ipmiFields["name"]),
|
||||
}
|
||||
|
||||
// tag the server is we have one
|
||||
if hostname != "" {
|
||||
tags["server"] = hostname
|
||||
}
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
if strings.EqualFold("ok", trim(ipmiFields["status_code"])) {
|
||||
fields["status"] = 1
|
||||
} else {
|
||||
fields["status"] = 0
|
||||
}
|
||||
|
||||
description := ipmiFields["description"]
|
||||
|
||||
// handle hex description field
|
||||
if strings.HasPrefix(description, "0x") {
|
||||
descriptionInt, err := strconv.ParseInt(description, 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fields["value"] = float64(descriptionInt)
|
||||
} else if strings.Index(description, " ") > 0 {
|
||||
// split middle column into value and unit
|
||||
valunit := strings.SplitN(description, " ", 2)
|
||||
var err error
|
||||
fields["value"], err = aToFloat(valunit[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(valunit) > 1 {
|
||||
tags["unit"] = transform(valunit[1])
|
||||
}
|
||||
} else {
|
||||
fields["value"] = 0.0
|
||||
}
|
||||
|
||||
acc.AddFields("ipmi_sensor", fields, tags, measuredAt)
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (m *Ipmi) parseV2(acc telegraf.Accumulator, hostname string, cmdOut []byte, measuredAt 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 := m.extractFieldsFromRegex(reV2ParseLine, 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 := m.extractFieldsFromRegex(reV2ParseDescription, 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 := m.extractFieldsFromRegex(reV2ParseUnit, 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, measuredAt)
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// extractFieldsFromRegex consumes a regex with named capture groups and returns a kvp map of strings with the results
|
||||
func (m *Ipmi) extractFieldsFromRegex(re *regexp.Regexp, input string) map[string]string {
|
||||
submatches := re.FindStringSubmatch(input)
|
||||
results := make(map[string]string)
|
||||
subexpNames := re.SubexpNames()
|
||||
if len(subexpNames) > len(submatches) {
|
||||
m.Log.Debugf("No matches found in %q", input)
|
||||
return results
|
||||
}
|
||||
for i, name := range 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) {
|
||||
f, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func sanitizeIPMICmd(args []string) []string {
|
||||
for i, v := range args {
|
||||
if v == "-P" {
|
||||
args[i+1] = "REDACTED"
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func trim(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func transform(s string) string {
|
||||
s = trim(s)
|
||||
s = strings.ToLower(s)
|
||||
return strings.ReplaceAll(s, " ", "_")
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("ipmi_sensor", func() telegraf.Input {
|
||||
return &Ipmi{Timeout: config.Duration(20 * time.Second)}
|
||||
})
|
||||
}
|
936
plugins/inputs/ipmi_sensor/ipmi_sensor_test.go
Normal file
936
plugins/inputs/ipmi_sensor/ipmi_sensor_test.go
Normal file
|
@ -0,0 +1,936 @@
|
|||
package ipmi_sensor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
i := &Ipmi{
|
||||
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
|
||||
Path: "ipmitool",
|
||||
Privilege: "USER",
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
HexKey: "1234567F",
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommand
|
||||
var acc testutil.Accumulator
|
||||
|
||||
require.NoError(t, i.Init())
|
||||
require.NoError(t, acc.GatherError(i.Gather))
|
||||
require.EqualValues(t, 262, acc.NFields(), "non-numeric measurements should be ignored")
|
||||
|
||||
conn := newConnection(i.Servers[0], i.Privilege, i.HexKey)
|
||||
require.EqualValues(t, "USERID", conn.username)
|
||||
require.EqualValues(t, "lan", conn.intf)
|
||||
require.EqualValues(t, "1234567F", conn.hexKey)
|
||||
|
||||
var testsWithServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(20),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "ambient_temp",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(80),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "altitude",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "feet",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(210),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "avg_power",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "watts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(4.9),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_5v",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(3.05),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_vbat",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(2610),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1a_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(1775),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1b_tach",
|
||||
"server": "192.168.1.1",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
|
||||
i = &Ipmi{
|
||||
Path: "ipmitool",
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
require.NoError(t, i.Init())
|
||||
require.NoError(t, acc.GatherError(i.Gather))
|
||||
|
||||
var testsWithoutServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(20),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "ambient_temp",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(80),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "altitude",
|
||||
"unit": "feet",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(210),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "avg_power",
|
||||
"unit": "watts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(4.9),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_5v",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(3.05),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "planar_vbat",
|
||||
"unit": "volts",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(2610),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1a_tach",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(1775),
|
||||
"status": 1,
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan_1b_tach",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithoutServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeExecCommand is a helper function that mock
|
||||
// the exec.Command call (and call the test binary)
|
||||
func fakeExecCommand(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcess", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TestHelperProcess isn't a real test. It's used to mock exec.Command
|
||||
// For example, if you run:
|
||||
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcess -- chrony tracking
|
||||
// it returns below mockData.
|
||||
func TestHelperProcess(_ *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
mockData := `Ambient Temp | 20 degrees C | ok
|
||||
Altitude | 80 feet | ok
|
||||
Avg Power | 210 Watts | ok
|
||||
Planar 3.3V | 3.29 Volts | ok
|
||||
Planar 5V | 4.90 Volts | ok
|
||||
Planar 12V | 12.04 Volts | ok
|
||||
Planar VBAT | 3.05 Volts | ok
|
||||
Fan 1A Tach | 2610 RPM | ok
|
||||
Fan 1B Tach | 1775 RPM | ok
|
||||
Fan 2A Tach | 2001 RPM | ok
|
||||
Fan 2B Tach | 1275 RPM | ok
|
||||
Fan 3A Tach | 2929 RPM | ok
|
||||
Fan 3B Tach | 2125 RPM | ok
|
||||
Fan 1 | 0x00 | ok
|
||||
Fan 2 | 0x00 | ok
|
||||
Fan 3 | 0x00 | ok
|
||||
Front Panel | 0x00 | ok
|
||||
Video USB | 0x00 | ok
|
||||
DASD Backplane 1 | 0x00 | ok
|
||||
SAS Riser | 0x00 | ok
|
||||
PCI Riser 1 | 0x00 | ok
|
||||
PCI Riser 2 | 0x00 | ok
|
||||
CPU 1 | 0x00 | ok
|
||||
CPU 2 | 0x00 | ok
|
||||
All CPUs | 0x00 | ok
|
||||
One of The CPUs | 0x00 | ok
|
||||
IOH Temp Status | 0x00 | ok
|
||||
CPU 1 OverTemp | 0x00 | ok
|
||||
CPU 2 OverTemp | 0x00 | ok
|
||||
CPU Fault Reboot | 0x00 | ok
|
||||
Aux Log | 0x00 | ok
|
||||
NMI State | 0x00 | ok
|
||||
ABR Status | 0x00 | ok
|
||||
Firmware Error | 0x00 | ok
|
||||
PCIs | 0x00 | ok
|
||||
CPUs | 0x00 | ok
|
||||
DIMMs | 0x00 | ok
|
||||
Sys Board Fault | 0x00 | ok
|
||||
Power Supply 1 | 0x00 | ok
|
||||
Power Supply 2 | 0x00 | ok
|
||||
PS 1 Fan Fault | 0x00 | ok
|
||||
PS 2 Fan Fault | 0x00 | ok
|
||||
VT Fault | 0x00 | ok
|
||||
Pwr Rail A Fault | 0x00 | ok
|
||||
Pwr Rail B Fault | 0x00 | ok
|
||||
Pwr Rail C Fault | 0x00 | ok
|
||||
Pwr Rail D Fault | 0x00 | ok
|
||||
Pwr Rail E Fault | 0x00 | ok
|
||||
PS 1 Therm Fault | 0x00 | ok
|
||||
PS 2 Therm Fault | 0x00 | ok
|
||||
PS1 12V OV Fault | 0x00 | ok
|
||||
PS2 12V OV Fault | 0x00 | ok
|
||||
PS1 12V UV Fault | 0x00 | ok
|
||||
PS2 12V UV Fault | 0x00 | ok
|
||||
PS1 12V OC Fault | 0x00 | ok
|
||||
PS2 12V OC Fault | 0x00 | ok
|
||||
PS 1 VCO Fault | 0x00 | ok
|
||||
PS 2 VCO Fault | 0x00 | ok
|
||||
Power Unit | 0x00 | ok
|
||||
Cooling Zone 1 | 0x00 | ok
|
||||
Cooling Zone 2 | 0x00 | ok
|
||||
Cooling Zone 3 | 0x00 | ok
|
||||
Drive 0 | 0x00 | ok
|
||||
Drive 1 | 0x00 | ok
|
||||
Drive 2 | 0x00 | ok
|
||||
Drive 3 | 0x00 | ok
|
||||
Drive 4 | 0x00 | ok
|
||||
Drive 5 | 0x00 | ok
|
||||
Drive 6 | 0x00 | ok
|
||||
Drive 7 | 0x00 | ok
|
||||
Drive 8 | 0x00 | ok
|
||||
Drive 9 | 0x00 | ok
|
||||
Drive 10 | 0x00 | ok
|
||||
Drive 11 | 0x00 | ok
|
||||
Drive 12 | 0x00 | ok
|
||||
Drive 13 | 0x00 | ok
|
||||
Drive 14 | 0x00 | ok
|
||||
Drive 15 | 0x00 | ok
|
||||
All DIMMS | 0x00 | ok
|
||||
One of the DIMMs | 0x00 | ok
|
||||
DIMM 1 | 0x00 | ok
|
||||
DIMM 2 | 0x00 | ok
|
||||
DIMM 3 | 0x00 | ok
|
||||
DIMM 4 | 0x00 | ok
|
||||
DIMM 5 | 0x00 | ok
|
||||
DIMM 6 | 0x00 | ok
|
||||
DIMM 7 | 0x00 | ok
|
||||
DIMM 8 | 0x00 | ok
|
||||
DIMM 9 | 0x00 | ok
|
||||
DIMM 10 | 0x00 | ok
|
||||
DIMM 11 | 0x00 | ok
|
||||
DIMM 12 | 0x00 | ok
|
||||
DIMM 13 | 0x00 | ok
|
||||
DIMM 14 | 0x00 | ok
|
||||
DIMM 15 | 0x00 | ok
|
||||
DIMM 16 | 0x00 | ok
|
||||
DIMM 17 | 0x00 | ok
|
||||
DIMM 18 | 0x00 | ok
|
||||
DIMM 1 Temp | 0x00 | ok
|
||||
DIMM 2 Temp | 0x00 | ok
|
||||
DIMM 3 Temp | 0x00 | ok
|
||||
DIMM 4 Temp | 0x00 | ok
|
||||
DIMM 5 Temp | 0x00 | ok
|
||||
DIMM 6 Temp | 0x00 | ok
|
||||
DIMM 7 Temp | 0x00 | ok
|
||||
DIMM 8 Temp | 0x00 | ok
|
||||
DIMM 9 Temp | 0x00 | ok
|
||||
DIMM 10 Temp | 0x00 | ok
|
||||
DIMM 11 Temp | 0x00 | ok
|
||||
DIMM 12 Temp | 0x00 | ok
|
||||
DIMM 13 Temp | 0x00 | ok
|
||||
DIMM 14 Temp | 0x00 | ok
|
||||
DIMM 15 Temp | 0x00 | ok
|
||||
DIMM 16 Temp | 0x00 | ok
|
||||
DIMM 17 Temp | 0x00 | ok
|
||||
DIMM 18 Temp | 0x00 | ok
|
||||
PCI 1 | 0x00 | ok
|
||||
PCI 2 | 0x00 | ok
|
||||
PCI 3 | 0x00 | ok
|
||||
PCI 4 | 0x00 | ok
|
||||
All PCI Error | 0x00 | ok
|
||||
One of PCI Error | 0x00 | ok
|
||||
IPMI Watchdog | 0x00 | ok
|
||||
Host Power | 0x00 | ok
|
||||
DASD Backplane 2 | 0x00 | ok
|
||||
DASD Backplane 3 | Not Readable | ns
|
||||
DASD Backplane 4 | Not Readable | ns
|
||||
Backup Memory | 0x00 | ok
|
||||
Progress | 0x00 | ok
|
||||
Planar Fault | 0x00 | ok
|
||||
SEL Fullness | 0x00 | ok
|
||||
PCI 5 | 0x00 | ok
|
||||
OS RealTime Mod | 0x00 | ok
|
||||
`
|
||||
|
||||
args := os.Args
|
||||
|
||||
// Previous arguments are tests stuff, that looks like :
|
||||
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
|
||||
cmd := args[3]
|
||||
|
||||
// Ignore the returned errors for the mocked interface as tests will fail anyway
|
||||
if cmd != "ipmitool" {
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Fprint(os.Stdout, mockData)
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestGatherV2(t *testing.T) {
|
||||
i := &Ipmi{
|
||||
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
|
||||
Path: "ipmitool",
|
||||
Privilege: "USER",
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
MetricVersion: 2,
|
||||
HexKey: "0000000F",
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
// overwriting exec commands with mock commands
|
||||
execCommand = fakeExecCommandV2
|
||||
var acc testutil.Accumulator
|
||||
|
||||
require.NoError(t, i.Init())
|
||||
require.NoError(t, acc.GatherError(i.Gather))
|
||||
|
||||
conn := newConnection(i.Servers[0], i.Privilege, i.HexKey)
|
||||
require.EqualValues(t, "USERID", conn.username)
|
||||
require.EqualValues(t, "lan", conn.intf)
|
||||
require.EqualValues(t, "0000000F", conn.hexKey)
|
||||
|
||||
var testsWithServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
// SEL | 72h | ns | 7.1 | No Reading
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(0),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "sel",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ns",
|
||||
"status_desc": "no_reading",
|
||||
"server": "192.168.1.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
|
||||
i = &Ipmi{
|
||||
Path: "ipmitool",
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
MetricVersion: 2,
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
require.NoError(t, i.Init())
|
||||
require.NoError(t, acc.GatherError(i.Gather))
|
||||
|
||||
var testsWithoutServer = []struct {
|
||||
fields map[string]interface{}
|
||||
tags map[string]string
|
||||
}{
|
||||
// SEL | 72h | ns | 7.1 | No Reading
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(0),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "sel",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ns",
|
||||
"status_desc": "no_reading",
|
||||
},
|
||||
},
|
||||
// Intrusion | 73h | ok | 7.1 |
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(0),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "intrusion",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ok",
|
||||
"status_desc": "ok",
|
||||
},
|
||||
},
|
||||
// Fan1 | 30h | ok | 7.1 | 5040 RPM
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(5040),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "fan1",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ok",
|
||||
"unit": "rpm",
|
||||
},
|
||||
},
|
||||
// Inlet Temp | 04h | ok | 7.1 | 25 degrees C
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(25),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "inlet_temp",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ok",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
},
|
||||
// USB Cable Pres | 50h | ok | 7.1 | Connected
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(0),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "usb_cable_pres",
|
||||
"entity_id": "7.1",
|
||||
"status_code": "ok",
|
||||
"status_desc": "connected",
|
||||
},
|
||||
},
|
||||
// Current 1 | 6Ah | ok | 10.1 | 7.20 Amps
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(7.2),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "current_1",
|
||||
"entity_id": "10.1",
|
||||
"status_code": "ok",
|
||||
"unit": "amps",
|
||||
},
|
||||
},
|
||||
// Power Supply 1 | 03h | ok | 10.1 | 110 Watts, Presence detected
|
||||
{
|
||||
map[string]interface{}{
|
||||
"value": float64(110),
|
||||
},
|
||||
map[string]string{
|
||||
"name": "power_supply_1",
|
||||
"entity_id": "10.1",
|
||||
"status_code": "ok",
|
||||
"unit": "watts",
|
||||
"status_desc": "presence_detected",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testsWithoutServer {
|
||||
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeExecCommandV2 is a helper function that mock
|
||||
// the exec.Command call (and call the test binary)
|
||||
func fakeExecCommandV2(command string, args ...string) *exec.Cmd {
|
||||
cs := []string{"-test.run=TestHelperProcessV2", "--", command}
|
||||
cs = append(cs, args...)
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TestHelperProcessV2 isn't a real test. It's used to mock exec.Command
|
||||
// For example, if you run:
|
||||
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcessV2 -- chrony tracking
|
||||
// it returns below mockData.
|
||||
func TestHelperProcessV2(_ *testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
// Curated list of use cases instead of full dumps
|
||||
mockData := `SEL | 72h | ns | 7.1 | No Reading
|
||||
Intrusion | 73h | ok | 7.1 |
|
||||
Fan1 | 30h | ok | 7.1 | 5040 RPM
|
||||
Inlet Temp | 04h | ok | 7.1 | 25 degrees C
|
||||
USB Cable Pres | 50h | ok | 7.1 | Connected
|
||||
Current 1 | 6Ah | ok | 10.1 | 7.20 Amps
|
||||
Power Supply 1 | 03h | ok | 10.1 | 110 Watts, Presence detected
|
||||
`
|
||||
|
||||
args := os.Args
|
||||
|
||||
// Previous arguments are tests stuff, that looks like :
|
||||
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
|
||||
cmd := args[3]
|
||||
|
||||
// Ignore the returned errors for the mocked interface as tests will fail anyway
|
||||
if cmd != "ipmitool" {
|
||||
fmt.Fprint(os.Stdout, "command not found")
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Fprint(os.Stdout, mockData)
|
||||
//nolint:revive // error code is important for this "test"
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestExtractFields(t *testing.T) {
|
||||
v1Data := `Ambient Temp | 20 degrees C | ok
|
||||
Altitude | 80 feet | ok
|
||||
Avg Power | 210 Watts | ok
|
||||
Planar 3.3V | 3.29 Volts | ok
|
||||
Planar 5V | 4.90 Volts | ok
|
||||
Planar 12V | 12.04 Volts | ok
|
||||
B | 0x00 | ok
|
||||
Unable to send command: Invalid argument
|
||||
ECC Corr Err | Not Readable | ns
|
||||
Unable to send command: Invalid argument
|
||||
ECC Uncorr Err | Not Readable | ns
|
||||
Unable to send command: Invalid argument
|
||||
`
|
||||
|
||||
v2Data := `SEL | 72h | ns | 7.1 | No Reading
|
||||
Intrusion | 73h | ok | 7.1 |
|
||||
Fan1 | 30h | ok | 7.1 | 5040 RPM
|
||||
Inlet Temp | 04h | ok | 7.1 | 25 degrees C
|
||||
USB Cable Pres | 50h | ok | 7.1 | Connected
|
||||
Unable to send command: Invalid argument
|
||||
Current 1 | 6Ah | ok | 10.1 | 7.20 Amps
|
||||
Unable to send command: Invalid argument
|
||||
Power Supply 1 | 03h | ok | 10.1 | 110 Watts, Presence detected
|
||||
`
|
||||
|
||||
tests := []string{
|
||||
v1Data,
|
||||
v2Data,
|
||||
}
|
||||
|
||||
ipmi := &Ipmi{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
t.Logf("Checking v%d data...", i+1)
|
||||
ipmi.extractFieldsFromRegex(reV1ParseLine, tests[i])
|
||||
ipmi.extractFieldsFromRegex(reV2ParseLine, tests[i])
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseV1(t *testing.T) {
|
||||
type args struct {
|
||||
hostname string
|
||||
cmdOut []byte
|
||||
measuredAt time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantFields map[string]interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test correct V1 parsing with hex code",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
measuredAt: time.Now(),
|
||||
cmdOut: []byte("PS1 Status | 0x02 | ok"),
|
||||
},
|
||||
wantFields: map[string]interface{}{"value": float64(2), "status": 1},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test correct V1 parsing with value with unit",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
measuredAt: time.Now(),
|
||||
cmdOut: []byte("Avg Power | 210 Watts | ok"),
|
||||
},
|
||||
wantFields: map[string]interface{}{"value": float64(210), "status": 1},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
ipmi := &Ipmi{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
|
||||
if err := ipmi.parseV1(&acc, tt.args.hostname, tt.args.cmdOut, tt.args.measuredAt); (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseV1() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
acc.AssertContainsFields(t, "ipmi_sensor", tt.wantFields)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseV2(t *testing.T) {
|
||||
type args struct {
|
||||
hostname string
|
||||
cmdOut []byte
|
||||
measuredAt time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected []telegraf.Metric
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test correct V2 parsing with analog value with unit",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
cmdOut: []byte("Power Supply 1 | 03h | ok | 10.1 | 110 Watts, Presence detected"),
|
||||
measuredAt: time.Now(),
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "power_supply_1",
|
||||
"status_code": "ok",
|
||||
"server": "host",
|
||||
"entity_id": "10.1",
|
||||
"unit": "watts",
|
||||
"status_desc": "presence_detected",
|
||||
},
|
||||
map[string]interface{}{"value": 110.0},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test correct V2 parsing without analog value",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
cmdOut: []byte("Intrusion | 73h | ok | 7.1 |"),
|
||||
measuredAt: time.Now(),
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "intrusion",
|
||||
"status_code": "ok",
|
||||
"server": "host",
|
||||
"entity_id": "7.1",
|
||||
"status_desc": "ok",
|
||||
},
|
||||
map[string]interface{}{"value": 0.0},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "parse negative value",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
cmdOut: []byte("DIMM Thrm Mrgn 1 | B0h | ok | 8.1 | -55 degrees C"),
|
||||
measuredAt: time.Now(),
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "dimm_thrm_mrgn_1",
|
||||
"status_code": "ok",
|
||||
"server": "host",
|
||||
"entity_id": "8.1",
|
||||
"unit": "degrees_c",
|
||||
},
|
||||
map[string]interface{}{"value": -55.0},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
ipmi := &Ipmi{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
if err := ipmi.parseV2(&acc, tt.args.hostname, tt.args.cmdOut, tt.args.measuredAt); (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseV2() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parsePowerStatus(t *testing.T) {
|
||||
type args struct {
|
||||
hostname string
|
||||
cmdOut []byte
|
||||
measuredAt time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected []telegraf.Metric
|
||||
}{
|
||||
{
|
||||
name: "Test correct parse power status off",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
cmdOut: []byte("Chassis Power is off"),
|
||||
measuredAt: time.Now(),
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "chassis_power_status",
|
||||
"server": "host",
|
||||
},
|
||||
map[string]interface{}{"value": 0},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test correct parse power status on",
|
||||
args: args{
|
||||
hostname: "host",
|
||||
cmdOut: []byte("Chassis Power is on"),
|
||||
measuredAt: time.Now(),
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "chassis_power_status",
|
||||
"server": "host",
|
||||
},
|
||||
map[string]interface{}{"value": 1},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
err := parseChassisPowerStatus(&acc, tt.args.hostname, tt.args.cmdOut, tt.args.measuredAt)
|
||||
require.NoError(t, err)
|
||||
testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parsePowerReading(t *testing.T) {
|
||||
output := `Instantaneous power reading: 167 Watts
|
||||
Minimum during sampling period: 124 Watts
|
||||
Maximum during sampling period: 422 Watts
|
||||
Average power reading over sample period: 156 Watts
|
||||
IPMI timestamp: Mon Aug 1 21:22:51 2016
|
||||
Sampling period: 00699043 Seconds.
|
||||
Power reading state is: activated
|
||||
`
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "instantaneous_power_reading",
|
||||
"server": "host",
|
||||
"unit": "watts",
|
||||
},
|
||||
map[string]interface{}{"value": float64(167)},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "minimum_during_sampling_period",
|
||||
"server": "host",
|
||||
"unit": "watts",
|
||||
},
|
||||
map[string]interface{}{"value": float64(124)},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "maximum_during_sampling_period",
|
||||
"server": "host",
|
||||
"unit": "watts",
|
||||
},
|
||||
map[string]interface{}{"value": float64(422)},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
testutil.MustMetric("ipmi_sensor",
|
||||
map[string]string{
|
||||
"name": "average_power_reading_over_sample_period",
|
||||
"server": "host",
|
||||
"unit": "watts",
|
||||
},
|
||||
map[string]interface{}{"value": float64(156)},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
ipmi := &Ipmi{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
err := ipmi.parseDCMIPowerReading(&acc, "host", []byte(output), time.Now())
|
||||
require.NoError(t, err)
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
}
|
||||
|
||||
func TestSanitizeIPMICmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "default args",
|
||||
args: []string{
|
||||
"-H", "localhost",
|
||||
"-U", "username",
|
||||
"-P", "password",
|
||||
"-I", "lan",
|
||||
},
|
||||
expected: []string{
|
||||
"-H", "localhost",
|
||||
"-U", "username",
|
||||
"-P", "REDACTED",
|
||||
"-I", "lan",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no password",
|
||||
args: []string{
|
||||
"-H", "localhost",
|
||||
"-U", "username",
|
||||
"-I", "lan",
|
||||
},
|
||||
expected: []string{
|
||||
"-H", "localhost",
|
||||
"-U", "username",
|
||||
"-I", "lan",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty args",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sanitizedArgs := sanitizeIPMICmd(tt.args)
|
||||
require.Equal(t, tt.expected, sanitizedArgs)
|
||||
})
|
||||
}
|
||||
}
|
49
plugins/inputs/ipmi_sensor/sample.conf
Normal file
49
plugins/inputs/ipmi_sensor/sample.conf
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Read metrics from the bare metal servers via IPMI
|
||||
[[inputs.ipmi_sensor]]
|
||||
## Specify the path to the ipmitool executable
|
||||
# path = "/usr/bin/ipmitool"
|
||||
|
||||
## Use sudo
|
||||
## Setting 'use_sudo' to true will make use of sudo to run ipmitool.
|
||||
## Sudo must be configured to allow the telegraf user to run ipmitool
|
||||
## without a password.
|
||||
# use_sudo = false
|
||||
|
||||
## Servers
|
||||
## Specify one or more servers via a url. If no servers are specified, local
|
||||
## machine sensor stats will be queried. Uses the format:
|
||||
## [username[:password]@][protocol[(address)]]
|
||||
## e.g. root:passwd@lan(127.0.0.1)
|
||||
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]
|
||||
|
||||
## Session privilege level
|
||||
## Choose from: CALLBACK, USER, OPERATOR, ADMINISTRATOR
|
||||
# privilege = "ADMINISTRATOR"
|
||||
|
||||
## Timeout
|
||||
## Timeout for the ipmitool command to complete.
|
||||
# timeout = "20s"
|
||||
|
||||
## Metric schema version
|
||||
## See the plugin readme for more information on schema versioning.
|
||||
# metric_version = 1
|
||||
|
||||
## Sensors to collect
|
||||
## Choose from:
|
||||
## * sdr: default, collects sensor data records
|
||||
## * chassis_power_status: collects the power status of the chassis
|
||||
## * dcmi_power_reading: collects the power readings from the Data Center Management Interface
|
||||
# sensors = ["sdr"]
|
||||
|
||||
## Hex key
|
||||
## Optionally provide the hex key for the IMPI connection.
|
||||
# hex_key = ""
|
||||
|
||||
## Cache
|
||||
## If ipmitool should use a cache
|
||||
## Using a cache can speed up collection times depending on your device.
|
||||
# use_cache = false
|
||||
|
||||
## Path to the ipmitools cache file (defaults to OS temp dir)
|
||||
## The provided path must exist and must be writable
|
||||
# cache_path = ""
|
Loading…
Add table
Add a link
Reference in a new issue