Skip to content

Commit

Permalink
Add a plugin to get information from ipmitool
Browse files Browse the repository at this point in the history
- is_ipmi_device_available: check that your system supports IPMI by
  running `ipmitool power state`
    - returns true if IPMI is supported, false otherwise
- get_all_sensors: run `ipmitool sdr list`
    - returns a JSON string with all sensors or raise an error
- get_sensor: run `ipmitool sdr get <sensor>`
    - returns details about sensors passed as parameter or raise an
      error
- get_ipmi_lan: run `ipmitool lan print`
    - returns network info of the IPMI server as a JSON or
      raise an error

Signed-off-by: Guillaume <[email protected]>
  • Loading branch information
gthvn1 committed Oct 24, 2024
1 parent 155fa07 commit 65aa4e8
Show file tree
Hide file tree
Showing 3 changed files with 541 additions and 0 deletions.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,91 @@ $ xe host-call-plugin host-uuid=<uuid> plugin=hyperthreading.py fn=get_hyperthre
true
```

## Ipmitool

A xapi plugin that uses `ipmitool` to get information about sensors and the IPMI server. Before
running the commands you need to ensure that your system have support for IPMI.

### `is_ipmi_device_available`

Returns `true` if IPMI device is found and `ipmitool` can be used. If it could not open device at `/dev/ipmi*`
it returns `false`. In that case you need to ensure the IPMI module is loaded and that your system
supports IPMI. Others unexpected errors raise a XenAPIPlugin error.

```
$ xe host-call-plugin host-uuid=<uuid> plugin=ipmitool.py fn=is_ipmi_device_available
false
```

If `true` is returned you should be able to run `get_all_sensors`, `get_sensor`
or `get_ipmi_lan` without raising a XenAPIPlugin error.

### `get_all_sensors`

Returns a JSON containing all sensor data repository entries and readings or raise a XenAPIPlugin error.
```
$ xe host-call-plugin host-uuid=<uuid> plugin=ipmitool.py fn=get_all_sensors
[
{"name": "Fan1A", "value": "10920 RPM", "event": "ok"},
{"name": "Fan2A", "value": "10800 RPM", "event": "ok"},
{"name": "Inlet Temp", "value": "23 degrees C", "event": "ok"},
{"name": "Exhaust Temp", "value": "28 degrees C", "event": "ok"},
{"name": "Temp", "value": "38 degrees C", "event": "ok"}
{"name": "PFault Fail Safe", "value": "Not Readable", "event": "ns"}
...
]
```

### `get_sensor`

Returns a JSON containing detailed information about the sensors passed as paramaters
or raise an XenAPIPlugin error. The names of the sensors can be found by running `get_all_sensors`
function. If a wrong sensor name is passed an error is logged in `/var/log/ipmitool-xapi-plugin-plugin.log`
and the sensor is skipped.

```
$ xe host-call-plugin host-uuid=<uuid> plugin=ipmitool.py fn=get_sensor args:sensors="Fan7B,PFault Fail Safe"
[
{
"name": "Fan7B",
"info": [{"name": "Sensor ID", "value": "Fan7B (0x3d)"}, {"name": "Entity ID", "value": "7.1 (System Board)"}, {"name": "Sensor Type (Threshold)", "value": "Fan (0x04)"}, {"name": "Sensor Reading", "value": "10320 (+/- 120) RPM"}, {"name": "Status", "value": "ok"}, {"name": "Nominal Reading", "value": "6720.000"}, {"name": "Normal Minimum", "value": "16680.000"}, {"name": "Normal Maximum", "value": "23640.000"}, {"name": "Lower critical", "value": "720.000"}, {"name": "Lower non-critical", "value": "840.000"}, {"name": "Positive Hysteresis", "value": "120.000"}, {"name": "Negative Hysteresis", "value": "120.000"}, {"name": "Minimum sensor range", "value": "Unspecified"}, {"name": "Maximum sensor range", "value": "Unspecified"}, {"name": "Event Message Control", "value": "Per-threshold"}, {"name": "Readable Thresholds", "value": "lcr lnc"}, {"name": "Settable Thresholds", "value": ""}, {"name": "Threshold Read Mask", "value": "lcr lnc"}, {"name": "Assertion Events", "value": ""}, {"name": "Assertions Enabled", "value": "lnc- lcr-"}, {"name": "Deassertions Enabled", "value": "lnc- lcr-"}]
},
{
"name": "PFault Fail Safe",
"info": [{"name": "Sensor ID", "value": "PFault Fail Safe (0x66)"}, {"name": "Entity ID", "value": "7.1 (System Board)"}, {"name": "Sensor Type (Discrete)", "value": "Voltage (0x02)"}, {"name": "Sensor Reading", "value": "No Reading"}, {"name": "Event Message Control", "value": "Per-threshold"}, {"name": "OEM", "value": "0"}]
}
]
```

### `get_ipmi_lan`

Returns JSON that contains information about the configuration of the network related to the IPMI server
or raise a XenAPIPlugin error.

```
$ xe host-call-plugin host-uuid=<uuid> plugin=ipmitool.py fn=get_ipmi_lan
[
{
"name": "IP Address Source",
"value": "Static Address"
},
{
"name": "IP Address",
"value": "1.2.3.4"
},
{
"name": "Subnet Mask",
"value": "255.255.255.0"
},
{
"name": "MAC Address",
"value": "a8:ac:a2:a5:a0:ae"
},
...
]
```

## Tests

To run the plugins' unit tests you'll need to install `pytest`, `pyfakefs` and `mock`.
Expand Down
136 changes: 136 additions & 0 deletions SOURCES/etc/xapi.d/plugins/ipmitool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import sys
import XenAPIPlugin

sys.path.append(".")
from xcpngutils import (
configure_logging,
run_command,
error_wrapped,
ProcessException,
raise_plugin_error,
)


@error_wrapped
def _is_ipmi_available():
# Try to run a simple command to check if ipmitool works
# If the command raise an error saying that we cannot open the IPMI device it means
# that IPMI is not available on the system. So we return False. If we don't know the
# error we raise it again and it will need to be debugged...
try:
_ = run_command(["ipmitool", "chassis", "status"])
except ProcessException as e:
if "Could not open device" in e.stderr:
return False
else:
raise e

return True


def is_ipmi_device_available(_session, _args):
return json.dumps(_is_ipmi_available())


@error_wrapped
def check_ipmi_availability(func):
def wrapper(*args, **kwargs):
if not _is_ipmi_available():
raise_plugin_error(1, "IPMI not available")
return func(*args, **kwargs)

return wrapper


@check_ipmi_availability
def sensor_data(_session, _args):
sensor_data = []
output = run_command(["ipmitool", "sdr", "list"])

for line in output["stdout"].splitlines():
if not line:
continue
sensor_fields = line.split("|")
sensor_data.append({
"name": sensor_fields[0].strip(),
"value": sensor_fields[1].strip(),
"event": sensor_fields[2].strip(),
})

return json.dumps(sensor_data)


@check_ipmi_availability
def sensor_info(_session, args):
sensors_info = []
sensors = args.get("sensors")

if not sensors:
return "{}"

for sensor in sensors.split(","):
sensor = sensor.strip()
info = []
output = run_command(["ipmitool", "sdr", "get", sensor])

# If there is an error while getting info about the sensor skip it
# and report the error.
if output["stderr"]:
_LOGGER.error("{}".format(output["stderr"].rstrip()))
continue

for line in output["stdout"].splitlines():
if ":" not in line:
continue
name, value = line.split(":", 1)
info.append({
"name": name.strip(),
"value": value.strip(),
})

sensors_info.append({
"name": sensor,
"info": info,
})

return json.dumps(sensors_info)


@check_ipmi_availability
def ipmi_lan(_session, _args):
lan_info = []
wanted = [
"IP Address",
"Subnet Mask",
"MAC Address",
"BMC ARP Control",
"Default Gateway IP",
"802.1q VLAN",
"RMCP+ Cipher Suites",
]

output = run_command(["ipmitool", "lan", "print"])

for line in output["stdout"].splitlines():
if any(word in line for word in wanted):
name, value = line.split(":", 1)
lan_info.append({
"name": name.strip(),
"value": value.strip(),
})

return json.dumps(lan_info)


_LOGGER = configure_logging("ipmitool-xapi-plugin")
if __name__ == "__main__":
XenAPIPlugin.dispatch({
"is_ipmi_device_available": is_ipmi_device_available,
"get_all_sensors": sensor_data,
"get_sensor": sensor_info,
"get_ipmi_lan": ipmi_lan,
})
Loading

0 comments on commit 65aa4e8

Please sign in to comment.