diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 55a932df2..6c0d933c7 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -16,6 +16,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/haproxy" _ "github.com/influxdata/telegraf/plugins/inputs/httpjson" _ "github.com/influxdata/telegraf/plugins/inputs/influxdb" + _ "github.com/influxdata/telegraf/plugins/inputs/ipmi" _ "github.com/influxdata/telegraf/plugins/inputs/jolokia" _ "github.com/influxdata/telegraf/plugins/inputs/kafka_consumer" _ "github.com/influxdata/telegraf/plugins/inputs/leofs" diff --git a/plugins/inputs/ipmi/README.md b/plugins/inputs/ipmi/README.md new file mode 100644 index 000000000..ca42f7d10 --- /dev/null +++ b/plugins/inputs/ipmi/README.md @@ -0,0 +1,50 @@ +# Telegraf ipmi plugin + +Get bare metal metrics using the command line utility `ipmitool` + +see ipmitool(https://sourceforge.net/projects/ipmitool/files/ipmitool/) + +The plugin will use the following command to collect remote host sensor stats: + +ipmitool -I lan -H 192.168.1.1 -U USERID -P PASSW0RD sdr + +## Measurements + +- ipmi_sensor: + + * Tags: `server`,`host` + * Fields: + - status + - value + +## Configuration + +```toml +[[inputs.ipmi]] + ## specify servers via a url matching: + ## [username[:password]@][protocol[(address)]] + ## e.g. + ## root:passwd@lan(127.0.0.1) + ## + servers = ["USERID:PASSW0RD@lan(10.20.2.203)"] +``` + +## Output + +> ipmi_sensor,host=10.20.2.203,inst=Ambient\ Temp status=1i,value=20 1458488465012559455 +> ipmi_sensor,host=10.20.2.203,inst=Altitude status=1i,value=80 1458488465012688613 +> ipmi_sensor,host=10.20.2.203,inst=Avg\ Power status=1i,value=220 1458488465012776511 +> ipmi_sensor,host=10.20.2.203,inst=Planar\ 3.3V status=1i,value=3.28 1458488465012861875 +> ipmi_sensor,host=10.20.2.203,inst=Planar\ 5V status=1i,value=4.9 1458488465012944188 +> ipmi_sensor,host=10.20.2.203,inst=Planar\ 12V status=1i,value=12.04 1458488465013008485 +> ipmi_sensor,host=10.20.2.203,inst=Planar\ VBAT status=1i,value=3.04 1458488465013072508 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 1A\ Tach status=1i,value=2610 1458488465013137932 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 1B\ Tach status=1i,value=1775 1458488465013279896 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 2A\ Tach status=1i,value=1972 1458488465013358177 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 2B\ Tach status=1i,value=1275 1458488465013434023 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 3A\ Tach status=1i,value=2929 1458488465013514567 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 3B\ Tach status=1i,value=2125 1458488465013582616 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 1 status=1i,value=0 1458488465013643746 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 2 status=1i,value=0 1458488465013714887 +> ipmi_sensor,host=10.20.2.203,inst=Fan\ 3 status=1i,value=0 1458488465013861854 + diff --git a/plugins/inputs/ipmi/command.go b/plugins/inputs/ipmi/command.go new file mode 100644 index 000000000..4b3a2e81b --- /dev/null +++ b/plugins/inputs/ipmi/command.go @@ -0,0 +1,39 @@ +// command +package ipmi + +import ( + "bytes" + "fmt" + "os/exec" + "strings" +) + +type CommandRunner struct{} + +func (t CommandRunner) cmd(conn *Connection, args ...string) *exec.Cmd { + path := conn.Path + opts := append(conn.options(), args...) + + if path == "" { + path = "ipmitool" + } + + return exec.Command(path, opts...) + +} + +func (t CommandRunner) Run(conn *Connection, args ...string) (string, error) { + cmd := t.cmd(conn, args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("run %s %s: %s (%s)", + cmd.Path, strings.Join(cmd.Args, " "), stderr.String(), err) + } + + return stdout.String(), err +} diff --git a/plugins/inputs/ipmi/connection.go b/plugins/inputs/ipmi/connection.go new file mode 100644 index 000000000..de555a4c4 --- /dev/null +++ b/plugins/inputs/ipmi/connection.go @@ -0,0 +1,90 @@ +// connection +package ipmi + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +// Connection properties for a Client +type Connection struct { + Hostname string + Username string + Password string + Path string + Port int + Interface string +} + +func NewConnection(server string) *Connection { + conn := &Connection{} + inx1 := strings.Index(server, "@") + inx2 := strings.Index(server, "(") + inx3 := strings.Index(server, ")") + + connstr := server + + if inx1 > 0 { + security := server[0:inx1] + connstr = server[inx1+1 : len(server)] + up := strings.Split(security, ":") + conn.Username = up[0] + conn.Password = up[1] + } + + if inx2 > 0 { + inx2 = strings.Index(connstr, "(") + inx3 = strings.Index(connstr, ")") + + conn.Interface = connstr[0:inx2] + conn.Hostname = connstr[inx2+1 : inx3] + } + + return conn +} + +func (t *Connection) options() []string { + intf := t.Interface + if intf == "" { + intf = "lan" + } + + options := []string{ + "-H", t.Hostname, + "-U", t.Username, + "-P", t.Password, + "-I", intf, + } + + if t.Port != 0 { + options = append(options, "-p", strconv.Itoa(t.Port)) + } + + return options +} + +// RemoteIP returns the remote (bmc) IP address of the Connection +func (c *Connection) RemoteIP() string { + if net.ParseIP(c.Hostname) == nil { + addrs, err := net.LookupHost(c.Hostname) + if err != nil && len(addrs) > 0 { + return addrs[0] + } + } + return c.Hostname +} + +// LocalIP returns the local (client) IP address of the Connection +func (c *Connection) LocalIP() string { + conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", c.Hostname, c.Port)) + if err != nil { + // don't bother returning an error, since this value will never + // make it to the bmc if we can't connect to it. + return c.Hostname + } + _ = conn.Close() + host, _, _ := net.SplitHostPort(conn.LocalAddr().String()) + return host +} diff --git a/plugins/inputs/ipmi/ipmi.go b/plugins/inputs/ipmi/ipmi.go new file mode 100644 index 000000000..8a34c8d3f --- /dev/null +++ b/plugins/inputs/ipmi/ipmi.go @@ -0,0 +1,113 @@ +// ipmi +package ipmi + +import ( + "strconv" + "strings" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +type Ipmi struct { + Servers []string + runner Runner +} + +var sampleConfig = ` + ## specify servers via a url matching: + ## [username[:password]@][protocol[(address)]] + ## e.g. + ## root:passwd@lan(127.0.0.1) + ## + servers = ["USERID:PASSW0RD@lan(192.168.1.1)"] +` + +func NewIpmi() *Ipmi { + return &Ipmi{ + runner: CommandRunner{}, + } +} + +func (m *Ipmi) SampleConfig() string { + return sampleConfig +} + +func (m *Ipmi) Description() string { + return "Read metrics from one or many bare metal servers" +} + +func (m *Ipmi) Gather(acc telegraf.Accumulator) error { + if m.runner == nil { + m.runner = CommandRunner{} + } + for _, serv := range m.Servers { + err := m.gatherServer(serv, acc) + if err != nil { + return err + } + } + + return nil +} + +func (m *Ipmi) gatherServer(serv string, acc telegraf.Accumulator) error { + conn := NewConnection(serv) + + res, err := m.runner.Run(conn, "sdr") + if err != nil { + return err + } + + lines := strings.Split(res, "\n") + + for i := 0; i < len(lines); i++ { + vals := strings.Split(lines[i], "|") + if len(vals) == 3 { + tags := map[string]string{"server": conn.Hostname, "name": trim(vals[0])} + fields := make(map[string]interface{}) + if strings.EqualFold("ok", trim(vals[2])) { + fields["status"] = 1 + } else { + fields["status"] = 0 + } + + val1 := trim(vals[1]) + + if strings.Index(val1, " ") > 0 { + val := strings.Split(val1, " ")[0] + fields["value"] = Atofloat(val) + } else { + fields["value"] = 0.0 + } + + acc.AddFields("ipmi_sensor", fields, tags, time.Now()) + } + } + + return nil +} + +type Runner interface { + Run(conn *Connection, args ...string) (string, error) +} + +func Atofloat(val string) float64 { + f, err := strconv.ParseFloat(val, 64) + if err != nil { + return float64(0) + } else { + return float64(f) + } +} + +func trim(s string) string { + return strings.TrimSpace(s) +} + +func init() { + inputs.Add("ipmi", func() telegraf.Input { + return &Ipmi{} + }) +} diff --git a/plugins/inputs/ipmi/ipmi_test.go b/plugins/inputs/ipmi/ipmi_test.go new file mode 100644 index 000000000..1e0a57bd1 --- /dev/null +++ b/plugins/inputs/ipmi/ipmi_test.go @@ -0,0 +1,189 @@ +// ipmi_test +package ipmi + +import ( + "testing" + + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const serv = "USERID:PASSW0RD@lan(192.168.1.1)" + +const cmdReturn = ` +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 +` + +type runnerMock struct { + out string + err error +} + +func newRunnerMock(out string, err error) Runner { + return &runnerMock{ + out: out, + err: err, + } +} + +func (r runnerMock) Run(conn *Connection, args ...string) (out string, err error) { + if r.err != nil { + return out, r.err + } + return r.out, nil +} + +func TestIpmi(t *testing.T) { + i := &Ipmi{ + Servers: []string{"USERID:PASSW0RD@lan(192.168.1.1)"}, + runner: newRunnerMock(cmdReturn, nil), + } + + var acc testutil.Accumulator + + err := i.Gather(&acc) + + require.NoError(t, err) + + assert.Equal(t, acc.NFields(), 266, "non-numeric measurements should be ignored") +} + +func TestIpmiConnection(t *testing.T) { + conn := NewConnection(serv) + assert.Equal(t, "USERID", conn.Username) + assert.Equal(t, "lan", conn.Interface) + +}