Skip to content

Commit

Permalink
Add a plugin to get information from ipmitool
Browse files Browse the repository at this point in the history
- get_all_sensors: run `ipmitool sdr list`
    - returns a JSON string with all sensors
- get_sensor: run `ipmitool sdr get <sensor>`
    - returns details about sensors passed as parameter
- get_ipmi_lan: returns network info of the IPMI server as a JSON

Signed-off-by: Guillaume <[email protected]>
  • Loading branch information
gthvn1 committed Oct 17, 2024
1 parent 155fa07 commit e4a6140
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,71 @@ $ 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.

### get sensor data

Returns a JSON containing all sensor data repository entries and readings.
```
$ 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 details

Returns a JSON containing detailed information about the sensors passed as paramaters.
The names of the sensors can be found by running `get_all_sensors` function before.
```
$ 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 information

Returns JSON that contains information about the configuration of the network related to the IPMI server.
```
$ 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
101 changes: 101 additions & 0 deletions SOURCES/etc/xapi.d/plugins/ipmitool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/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


@error_wrapped
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)


@error_wrapped
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])

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)


@error_wrapped
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(
{
"get_all_sensors": sensor_data,
"get_sensor": sensor_info,
"get_ipmi_lan": ipmi_lan,
}
)
156 changes: 156 additions & 0 deletions tests/test_ipmitool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import json
import mock

from ipmitool import sensor_data, sensor_info, ipmi_lan

ipmitool_sdr_list = """
SEL | Not Readable | ns
Intrusion | 0x00 | ok
Fan1A | 4920 RPM | ok
Fan2A | 4920 RPM | ok
Fan3A | 4920 RPM | ok
Fan4A | 4680 RPM | ok
Fan5A | 4920 RPM | ok
Fan6A | 4920 RPM | ok
Inlet Temp | 24 degrees C | ok
Exhaust Temp | 35 degrees C | ok
Temp | 45 degrees C | ok
Temp | 42 degrees C | ok
"""

ipmitool_sdr_list_expected = [
{"name": "SEL", "value": "Not Readable", "event": "ns"},
{"name": "Intrusion", "value": "0x00", "event": "ok"},
{"name": "Fan1A", "value": "4920 RPM", "event": "ok"},
{"name": "Fan2A", "value": "4920 RPM", "event": "ok"},
{"name": "Fan3A", "value": "4920 RPM", "event": "ok"},
{"name": "Fan4A", "value": "4680 RPM", "event": "ok"},
{"name": "Fan5A", "value": "4920 RPM", "event": "ok"},
{"name": "Fan6A", "value": "4920 RPM", "event": "ok"},
{"name": "Inlet Temp", "value": "24 degrees C", "event": "ok"},
{"name": "Exhaust Temp", "value": "35 degrees C", "event": "ok"},
{"name": "Temp", "value": "45 degrees C", "event": "ok"},
{"name": "Temp", "value": "42 degrees C", "event": "ok"},
]

ipmitool_sdr_fan1 = """
Sensor ID : Fan1A (0x30)
Entity ID : 7.1 (System Board)
Sensor Type (Threshold) : Fan (0x04)
Sensor Reading : 4920 (+/- 120) RPM
Status : ok
Nominal Reading : 10080.000
Normal Minimum : 16680.000
Normal Maximum : 23640.000
Lower critical : 720.000
Lower non-critical : 840.000
Positive Hysteresis : 120.000
Negative Hysteresis : 120.000
Minimum sensor range : Unspecified
Maximum sensor range : Unspecified
Event Message Control : Per-threshold
Readable Thresholds : lcr lnc
Settable Thresholds :
Threshold Read Mask : lcr lnc
Assertion Events :
Assertions Enabled : lnc- lcr-
Deassertions Enabled : lnc- lcr-
"""

ipmitool_sdr_fan1_expected = [
{
"name": "Fan1A",
"info": [
{"name": "Sensor ID", "value": "Fan1A (0x30)"},
{"name": "Entity ID", "value": "7.1 (System Board)"},
{"name": "Sensor Type (Threshold)", "value": "Fan (0x04)"},
{"name": "Sensor Reading", "value": "4920 (+/- 120) RPM"},
{"name": "Status", "value": "ok"},
{"name": "Nominal Reading", "value": "10080.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-"},
],
}
]

ipmitool_lan_print = """
Set in Progress : Set Complete
Auth Type Support : MD5
Auth Type Enable : Callback : MD5
: User : MD5
: Operator : MD5
: Admin : MD5
: OEM :
IP Address Source : Static Address
IP Address : 172.16.1.2
Subnet Mask : 255.255.254.0
MAC Address : f8:bc:12:12:13:14
SNMP Community String : public
IP Header : TTL=0x40 Flags=0x40 Precedence=0x00 TOS=0x10
BMC ARP Control : ARP Responses Enabled, Gratuitous ARP Disabled
Gratituous ARP Intrvl : 2.0 seconds
Default Gateway IP : 172.16.210.1
Default Gateway MAC : 00:00:00:00:00:00
Backup Gateway IP : 0.0.0.0
Backup Gateway MAC : 00:00:00:00:00:00
802.1q VLAN ID : Disabled
802.1q VLAN Priority : 0
RMCP+ Cipher Suites : 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
Cipher Suite Priv Max : Xaaaaaaaaaaaaaa
: X=Cipher Suite Unused
: c=CALLBACK
: u=USER
: o=OPERATOR
: a=ADMIN
: O=OEM
Bad Password Threshold : Not Available
"""

ipmitool_lan_print_expected = [
{"name": "IP Address Source", "value": "Static Address"},
{"name": "IP Address", "value": "172.16.1.2"},
{"name": "Subnet Mask", "value": "255.255.254.0"},
{"name": "MAC Address", "value": "f8:bc:12:12:13:14"},
{
"name": "BMC ARP Control",
"value": "ARP Responses Enabled, Gratuitous ARP Disabled",
},
{"name": "Default Gateway IP", "value": "172.16.210.1"},
{"name": "802.1q VLAN ID", "value": "Disabled"},
{"name": "802.1q VLAN Priority", "value": "0"},
{"name": "RMCP+ Cipher Suites", "value": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14"},
]


@mock.patch("ipmitool.run_command", autospec=True)
class TestIpmitool:
def test_sensor_data(self, run_command):
run_command.return_value = {"stdout": ipmitool_sdr_list}

output = sensor_data(None, None)
assert output == json.dumps(ipmitool_sdr_list_expected)

def test_sensor_info(self, run_command):
run_command.return_value = {"stdout": ipmitool_sdr_fan1}

output = sensor_info(None, {"sensors": "Fan1A"})
assert output == json.dumps(ipmitool_sdr_fan1_expected)

def test_ipmi_lan(self, run_command):
run_command.return_value = {"stdout": ipmitool_lan_print}

output = ipmi_lan(None, None)
assert output == json.dumps(ipmitool_lan_print_expected)

0 comments on commit e4a6140

Please sign in to comment.