Hello, If it helps porting to a plugin or the base OS, the following script reads controller temperatures directly via ioctl without needing lsiutil or storcli. It talks to the device node created by the mpt3sas kernel module. I could only test with mpt3sas (9305-16i) as I don't have hardware using the older mpt/mpt2 drivers. Created with the help of generative AI, use at your own risk! #!/usr/bin/env python3
"""Read LSI SAS IOC temperature directly via ioctl (no lsiutil needed)."""
import ctypes
import fcntl
import os
import struct
import sys
# MPI constants
MPI_FUNCTION_CONFIG = 0x04
MPI_CONFIG_ACTION_PAGE_HEADER = 0x00
MPI_CONFIG_ACTION_PAGE_READ_CURRENT = 0x01
MPI_CONFIG_PAGETYPE_IO_UNIT = 0x00
IO_UNIT_PAGE_NUMBER = 7
# Temperature unit constants
TEMP_NOT_PRESENT = 0x00
TEMP_FAHRENHEIT = 0x01
TEMP_CELSIUS = 0x02
# ioctl numbers: _IOC(_IOC_READ|_IOC_WRITE, magic, nr, size)
# mptctl uses magic 'm' (0x6D), mpt2/3 use 'L' (0x4C), both nr=20
# sizeof(struct mpt_ioctl_command) = 72 on 64-bit (same for mpt2/mpt3 variants)
STRUCT_SIZE = 72
DEVICES = [
("/dev/mpt3ctl", ord("L"), 20),
("/dev/mpt2ctl", ord("L"), 20),
("/dev/mptctl", ord("m"), 20),
]
def _ioctl_number(magic, nr):
"""Compute ioctl request number: _IOWR(magic, nr, struct_size)."""
# _IOC(direction=3, type, nr, size) on Linux:
# direction bits [31:30], size [29:16], type [15:8], nr [7:0]
return (3 << 30) | (STRUCT_SIZE << 16) | (magic << 8) | nr
def _build_config_msg(action, page_length=0, page_number=IO_UNIT_PAGE_NUMBER,
page_type=MPI_CONFIG_PAGETYPE_IO_UNIT):
"""Build a Config request message (28 bytes, no SGE)."""
# Config_t layout (28 bytes):
# Action(1) AliasIndex(1) ChainOffset(1) Function(1)
# ExtPageLength(2) ExtPageType(1) MsgFlags(1)
# MsgContext(4)
# Reserved2(8)
# Header: PageVersion(1) PageLength(1) PageNumber(1) PageType(1)
# PageAddress(4)
msg = struct.pack(
"<BBBB HBB I 8s BBBB I",
action, # Action
0, # AliasIndex
0, # ChainOffset
MPI_FUNCTION_CONFIG, # Function
0, # ExtPageLength
0, # ExtPageType
0, # MsgFlags
0, # MsgContext
b"\x00" * 8, # Reserved2
0, # Header.PageVersion
page_length, # Header.PageLength (in dwords)
page_number, # Header.PageNumber
page_type, # Header.PageType
0, # PageAddress
)
return msg
def _do_config_ioctl(fd, ioctl_nr, iocnum, action, page_length=0, data_in_size=0):
"""Issue an MPTCOMMAND config ioctl. Returns (reply_bytes, data_in_bytes)."""
mf = _build_config_msg(action, page_length=page_length)
reply_buf = ctypes.create_string_buffer(128)
data_in_buf = ctypes.create_string_buffer(data_in_size) if data_in_size else None
# Build mpt_ioctl_command struct (manual packing for 64-bit):
# hdr.iocnum(4) hdr.port(4) hdr.maxDataSize(4) timeout(4)
# replyFrameBufPtr(8) dataInBufPtr(8) dataOutBufPtr(8) senseDataPtr(8)
# maxReplyBytes(4) dataInSize(4) dataOutSize(4) maxSenseBytes(4)
# dataSgeOffset(4) mf[28]
data_in_ptr = ctypes.addressof(data_in_buf) if data_in_buf else 0
cmd = struct.pack(
"<IIII QQQQ IIIII",
iocnum, # hdr.iocnum
0, # hdr.port
0, # hdr.maxDataSize
10, # timeout (seconds)
ctypes.addressof(reply_buf), # replyFrameBufPtr
data_in_ptr, # dataInBufPtr
0, # dataOutBufPtr (NULL - no writes!)
0, # senseDataPtr
128, # maxReplyBytes
data_in_size, # dataInSize
0, # dataOutSize (0 - no writes!)
0, # maxSenseBytes
7, # dataSgeOffset (dwords into mf where SGE goes)
)
cmd += mf
cmd_buf = ctypes.create_string_buffer(cmd, len(cmd))
try:
fcntl.ioctl(fd, ioctl_nr, cmd_buf)
except OSError as e:
return None, None
reply_bytes = bytes(reply_buf)
data_in_bytes = bytes(data_in_buf) if data_in_buf else b""
return reply_bytes, data_in_bytes
def read_ioc_temperature(fd, ioctl_nr, iocnum):
"""Read IO Unit Page 7 and return temperature in Celsius, or None."""
# Step 1: Get page header to learn page length
reply, _ = _do_config_ioctl(fd, ioctl_nr, iocnum, MPI_CONFIG_ACTION_PAGE_HEADER)
if reply is None:
return None
# ConfigReply: IOCStatus at offset 0x0E, Header at offset 0x14
ioc_status = struct.unpack_from("<H", reply, 0x0E)[0] & 0x7FFF
if ioc_status != 0:
return None
# Header: PageVersion(1) PageLength(1) PageNumber(1) PageType(1)
page_length = reply[0x15] # in 32-bit dwords
if page_length == 0:
return None
# Step 2: Read the actual page data
data_in_size = page_length * 4
reply, data = _do_config_ioctl(
fd, ioctl_nr, iocnum, MPI_CONFIG_ACTION_PAGE_READ_CURRENT,
page_length=page_length, data_in_size=data_in_size,
)
if reply is None or data is None:
return None
ioc_status = struct.unpack_from("<H", reply, 0x0E)[0] & 0x7FFF
if ioc_status != 0:
return None
# IO Unit Page 7: IOCTemperature at offset 0x10 (U16), IOCTemperatureUnits at 0x12 (U8)
if len(data) < 0x13:
return None
raw_temp = struct.unpack_from("<H", data, 0x10)[0]
units = data[0x12]
if units == TEMP_NOT_PRESENT:
return None
elif units == TEMP_FAHRENHEIT:
celsius = (raw_temp - 32) * 5 // 9
elif units == TEMP_CELSIUS:
celsius = raw_temp
else:
celsius = raw_temp # unknown units, return raw
return celsius
def main():
if os.geteuid() != 0:
print("Error: must run as root", file=sys.stderr)
sys.exit(1)
found_any = False
for dev_path, magic, nr in DEVICES:
if not os.path.exists(dev_path):
continue
ioctl_nr = _ioctl_number(magic, nr)
try:
fd = os.open(dev_path, os.O_RDWR)
except OSError:
continue
for iocnum in range(16):
temp = read_ioc_temperature(fd, ioctl_nr, iocnum)
if temp is not None:
print(f"ioc{iocnum}: {temp}C")
found_any = True
os.close(fd)
if not found_any:
print("No LSI IOC temperature sensors found", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()