telegraf/plugins/inputs/ipmi_sensor/ipmi_test.go

751 lines
20 KiB
Go

package ipmi_sensor
import (
"fmt"
"os"
"os/exec"
"testing"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGather(t *testing.T) {
i := &Ipmi{
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
Path: "ipmitool",
Privilege: "USER",
Timeout: internal.Duration{Duration: time.Second * 5},
}
// overwriting exec commands with mock commands
execCommand = fakeExecCommand
var acc testutil.Accumulator
err := acc.GatherError(i.Gather)
require.NoError(t, err)
assert.Equal(t, acc.NFields(), 262, "non-numeric measurements should be ignored")
conn := NewConnection(i.Servers[0], i.Privilege)
assert.Equal(t, "USERID", conn.Username)
assert.Equal(t, "lan", conn.Interface)
var testsWithServer = []struct {
fields map[string]interface{}
tags map[string]string
}{
{
map[string]interface{}{
"value": float64(20),
"status": int(1),
},
map[string]string{
"name": "ambient_temp",
"server": "192.168.1.1",
"unit": "degrees_c",
},
},
{
map[string]interface{}{
"value": float64(80),
"status": int(1),
},
map[string]string{
"name": "altitude",
"server": "192.168.1.1",
"unit": "feet",
},
},
{
map[string]interface{}{
"value": float64(210),
"status": int(1),
},
map[string]string{
"name": "avg_power",
"server": "192.168.1.1",
"unit": "watts",
},
},
{
map[string]interface{}{
"value": float64(4.9),
"status": int(1),
},
map[string]string{
"name": "planar_5v",
"server": "192.168.1.1",
"unit": "volts",
},
},
{
map[string]interface{}{
"value": float64(3.05),
"status": int(1),
},
map[string]string{
"name": "planar_vbat",
"server": "192.168.1.1",
"unit": "volts",
},
},
{
map[string]interface{}{
"value": float64(2610),
"status": int(1),
},
map[string]string{
"name": "fan_1a_tach",
"server": "192.168.1.1",
"unit": "rpm",
},
},
{
map[string]interface{}{
"value": float64(1775),
"status": int(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: internal.Duration{Duration: time.Second * 5},
}
err = acc.GatherError(i.Gather)
require.NoError(t, err)
var testsWithoutServer = []struct {
fields map[string]interface{}
tags map[string]string
}{
{
map[string]interface{}{
"value": float64(20),
"status": int(1),
},
map[string]string{
"name": "ambient_temp",
"unit": "degrees_c",
},
},
{
map[string]interface{}{
"value": float64(80),
"status": int(1),
},
map[string]string{
"name": "altitude",
"unit": "feet",
},
},
{
map[string]interface{}{
"value": float64(210),
"status": int(1),
},
map[string]string{
"name": "avg_power",
"unit": "watts",
},
},
{
map[string]interface{}{
"value": float64(4.9),
"status": int(1),
},
map[string]string{
"name": "planar_5v",
"unit": "volts",
},
},
{
map[string]interface{}{
"value": float64(3.05),
"status": int(1),
},
map[string]string{
"name": "planar_vbat",
"unit": "volts",
},
},
{
map[string]interface{}{
"value": float64(2610),
"status": int(1),
},
map[string]string{
"name": "fan_1a_tach",
"unit": "rpm",
},
},
{
map[string]interface{}{
"value": float64(1775),
"status": int(1),
},
map[string]string{
"name": "fan_1b_tach",
"unit": "rpm",
},
},
}
for _, test := range testsWithoutServer {
acc.AssertContainsTaggedFields(t, "ipmi_sensor", test.fields, test.tags)
}
}
// fackeExecCommand 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(t *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 := args[3], args[4:]
if cmd == "ipmitool" {
fmt.Fprint(os.Stdout, mockData)
} else {
fmt.Fprint(os.Stdout, "command not found")
os.Exit(1)
}
os.Exit(0)
}
func TestGatherV2(t *testing.T) {
i := &Ipmi{
Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"},
Path: "ipmitool",
Privilege: "USER",
Timeout: internal.Duration{Duration: time.Second * 5},
MetricVersion: 2,
}
// overwriting exec commands with mock commands
execCommand = fakeExecCommandV2
var acc testutil.Accumulator
err := acc.GatherError(i.Gather)
require.NoError(t, err)
conn := NewConnection(i.Servers[0], i.Privilege)
assert.Equal(t, "USERID", conn.Username)
assert.Equal(t, "lan", conn.Interface)
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: internal.Duration{Duration: time.Second * 5},
MetricVersion: 2,
}
err = acc.GatherError(i.Gather)
require.NoError(t, err)
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)
}
}
// fackeExecCommandV2 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(t *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 := args[3], args[4:]
if cmd == "ipmitool" {
fmt.Fprint(os.Stdout, mockData)
} else {
fmt.Fprint(os.Stdout, "command not found")
os.Exit(1)
}
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,
}
for i := range tests {
t.Logf("Checking v%d data...", i+1)
extractFieldsFromRegex(re_v1_parse_line, tests[i])
extractFieldsFromRegex(re_v2_parse_line, 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var acc testutil.Accumulator
if err := 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var acc testutil.Accumulator
if err := 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())
})
}
}