diff --git a/README.md b/README.md index c1aba22..b536b0e 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,71 @@ $ xe host-call-plugin host-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= 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= 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= 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`. diff --git a/SOURCES/etc/xapi.d/plugins/ipmitool.py b/SOURCES/etc/xapi.d/plugins/ipmitool.py new file mode 100755 index 0000000..240dd4f --- /dev/null +++ b/SOURCES/etc/xapi.d/plugins/ipmitool.py @@ -0,0 +1,142 @@ +#!/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, +) + + +@error_wrapped +def run_ipmitool(args): + try: + output = run_command(["ipmitool", "sdr", "list"]) + except ProcessException as e: + # Check if it is a "normal" error. + if "Could not open device" in e.stderr: + return ( + False, + "IPMI device not found. Ensure the IPMI module is loaded and that your system supports IPMI.", + ) + else: + raise e + else: + return True, output + + +@error_wrapped +def sensor_data(_session, _args): + sensor_data = [] + try: + success, output = run_ipmitool(["sdr", "list"]) + except ProcessException as e: + raise e + + if not success: + return json.dumps(output) + + 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 = [] + try: + success, output = run_ipmitool(["sdr", "get", sensor]) + except ProcessException as e: + raise e + + if not success: + return json.dumps(output) + + 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", + ] + + try: + success, output = run_ipmitool(["lan", "print"]) + except ProcessException as e: + raise e + + if not success: + return json.dumps(output) + + 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, + } + ) diff --git a/tests/test_ipmitool.py b/tests/test_ipmitool.py new file mode 100644 index 0000000..3f8ad8a --- /dev/null +++ b/tests/test_ipmitool.py @@ -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)