1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View 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
```

View 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
}

View 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())
}
}

View 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)}
})
}

View 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)
})
}
}

View 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 = ""