Skip to content

Commit

Permalink
add ipmi custom plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
snetsystems committed Sep 10, 2024
1 parent ecf94b1 commit 74eb195
Show file tree
Hide file tree
Showing 13 changed files with 3,111 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ process.yml
resource.syso
versioninfo.json
.uuid

telegraf_debug.exe
*.log
*.out
nul
184 changes: 184 additions & 0 deletions plugins/inputs/ipmi_sensor_ext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# IPMI Sensor Input Plugin

Get bare metal metrics using the command line utility
[`ipmitool`](https://github.com/ipmitool/ipmitool).

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

## 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]]
## optionally specify the path to the ipmitool executable
# path = "/usr/bin/ipmitool"
##
## 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
##
## optionally force session privilege level. Can be CALLBACK, USER, OPERATOR, ADMINISTRATOR
# privilege = "ADMINISTRATOR"
##
## optionally specify one or more servers via a url matching
## [username[:password]@][protocol[(address)]]
## e.g.
## root:passwd@lan(127.0.0.1)
##
## if no servers are specified, local machine sensor stats will be queried
##
# servers = ["USERID:PASSW0RD@lan(192.168.1.1)"]

## Recommended: use metric 'interval' that is a multiple of 'timeout' to avoid
## gaps or overlap in pulled data
interval = "30s"

## Timeout for the ipmitool command to complete. Default is 20 seconds.
timeout = "20s"

## Schema Version: (Optional, defaults to version 1)
metric_version = 2

## Optionally provide the hex key for the IMPI connection.
# hex_key = ""

## If ipmitool should use a cache
## for me ipmitool runs about 2 to 10 times faster with cache enabled on HP G10 servers (when using ubuntu20.04)
## the cache file may not work well for you if some sensors come up late
# 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 = ""
```

## 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
```
100 changes: 100 additions & 0 deletions plugins/inputs/ipmi_sensor_ext/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ipmi_sensor

import (
"fmt"
"net"
"strconv"
"strings"
)

// Connection properties for a Client
type Connection struct {
Hostname string
Username string
Password string
Port int
Interface 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.Interface = connstr[0:inx2]
conn.Hostname = connstr[inx2+1 : inx3]
}

return conn
}

func (c *Connection) options() []string {
intf := c.Interface
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
}

// 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()
//nolint:errcheck // unable to propagate
host, _, _ := net.SplitHostPort(conn.LocalAddr().String())
return host
}
87 changes: 87 additions & 0 deletions plugins/inputs/ipmi_sensor_ext/connection_test.go
Original file line number Diff line number Diff line change
@@ -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",
Interface: "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",
Interface: "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: "",
Interface: "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",
Interface: "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",
Interface: "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())
}
}
Loading

0 comments on commit 74eb195

Please sign in to comment.