#!/usr/bin/env python # vim: tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab # # Program that patches Dell PowerEdge BMC firmware files. # It allows to adjust hardcoded fan thresholds so that # fans can be swapped with other aftermarket models (e.g. # silent ones. # # Arnuschky, 2011 # http://projects.nuschkys.net/projects/dell-poweredge-2800/ # import sys import os import re from collections import namedtuple from struct import * # check python version if sys.version_info < (2,6,0): sys.stderr.write("You need python 2.6 or later to run this script\n") exit(1) # header of the container file size FILE_HEADER_SIZE = 17 # header for each subfile block size BLOCK_HEADER_SIZE = 51 # sensor block header size SENSOR_INFO_HEADER_SIZE = 5 # fan sensor data size (+string) FAN_SENSOR_INFO_DATA_SIZE = 43 # magic multiplier as in IPMI spec IMPI_VALUE_MULTIPLIER = 75 # CRC (plain) table # thanks to pycrc! http://www.tty1.net/pycrc/index_en.html CRC16_TABLE = [ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 ] # CRC (XModem) table # thanks to pycrc! http://www.tty1.net/pycrc/index_en.html CRC16_XMODEM_TABLE = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ] # mapping between system codes and display name # Note: these seem to be cities systems = dict() systems['BB'] = 'PowerEdge 2850' #systems['BRCL'] = 'unknown PowerEdge' systems['BRLN'] = 'PowerEdge 1950' systems['BULN'] = 'PowerEdge 2900 (Gen III)' systems['CAL'] = 'PowerEdge R300' #systems['CAT'] = 'unknown PowerEdge' #systems['DEF'] = 'unknown PowerEdge' #systems['EXPE'] = 'unknown PowerEdge' #systems['FT'] = 'unknown PowerEdge' #systems['FTT'] = 'unknown PowerEdge' #systems['GL'] = 'unknown PowerEdge' systems['GNS'] = 'PowerEdge SC1435' #systems['GUAD'] = 'unknown PowerEdge' #systems['HEL'] = 'unknown PowerEdge' systems['K_C'] = 'PowerEdge 2800' systems['LOND'] = 'PowerEdge 2950' #systems['MC'] = 'unknown PowerEdge' systems['MEC'] = 'PowerEdge 1900 (Gen I)' systems['MEL'] = 'PowerEdge 850' #systems['MIR'] = 'unknown PowerEdge' systems['MNTRL'] = 'PowerEdge 2900' systems['MUS'] = 'PowerEdge 1800' systems['NAG'] = 'Poweredge SC1425' #systems['OSLO'] = 'unknown PowerEdge' systems['SEO'] = 'PowerEdge 860' #systems['SF'] = 'unknown PowerEdge' systems['STL'] = 'PowerEdge 840' #systems['VANG'] = 'unknown PowerEdge' systems['VESO'] = 'PowerEdge R805' ########################### # Structure of the main container file header # class FileHeader(): def __init__(self, unpackdata): self.hex02 = unpackdata[0] self.numBlocks = unpackdata[1] self.filesize = unpackdata[2] self.zero = unpackdata[3] self.dellHeaderStr = unpackdata[4] def __str__(self): return "File header: numBlocks=%d filesize=%d dellHeaderStr='%s'" % (self.numBlocks, self.filesize, self.dellHeaderStr) ########################### # Structure of a block header (subfile definition) # class BlockHeader(): def __init__(self, unpackdata): self.zero1 = unpackdata[0] self.btype = unpackdata[1] self.zero2 = unpackdata[2] self.systemId = unpackdata[3] self.zero3 = unpackdata[4] self.zero4 = unpackdata[5] self.zero5 = unpackdata[6] self.unknownFixed = unpackdata[7] self.crc16 = unpackdata[8] self.length = unpackdata[9] self.offset = unpackdata[10] self.filename = unpackdata[11] def __str__(self): return "Block header: type=%d systemId=%d crc=0x%04x length=%d offset=%d filename='%s'" % (self.btype, self.systemId, self.crc16, self.length, self.offset, self.filename) ########################### # Structure of a header for each sensor info block # class SensorInfoHeader(): def __init__(self, unpackdata): self.blockId = unpackdata[0] self.tag0051 = unpackdata[1] self.sclass = unpackdata[2] self.length = unpackdata[3] ########################### # Structure of a fan sensor info block # class FanSensorInfoBlock(): def __init__(self, unpackdata): self.fill2000 = unpackdata[0] self.sensorId = unpackdata[1] self.unknown1 = unpackdata[2] self.threshold = unpackdata[3] self.unknown2 = unpackdata[4] self.fill05050000 = unpackdata[5] self.thingy = unpackdata[6] self.name = unpackdata[7] ########################### # Object that holds the firmware file info # (header and filename) # class Firmware: def __init__(self): self.ffile = None self.header = None ########################### # Object defining everything we know about the # PowerEdge system we want to patch # class PowerEdgeSystem: def __init__(self): self.idStr = "" self.sensorBlockId = 0 self.sensorBlockHeader = None self.numFans = 0 self.fanNames = list() self.fanThresholds = list() self.fanSpeeds = list() self.sensorNumbers = list() self.unknown = False ######################################################## # computes CRC-16 (plain) # used in main container file (last two bytes) # and once for each subfile (stored in the header) # @return crc 16bit # def crc16(data, crc): for byte in data: crc = (crc >> 8) ^ CRC16_TABLE[(crc ^ ord(byte)) & 0xff] return crc ######################################################## # Computes CRC-16 (Xmodem) # Used for each sensor info block (last two bytes of block) # @return crc 16bit # def crc16xmodem(data, crc): for byte in data: crc = ((crc << 8) & 0xffff) ^ CRC16_XMODEM_TABLE[((crc >> 8) ^ ord(byte)) & 0xff] return crc ######################################################## # Checks that the main container file is valid. # Checks filesize, CRC, header string/structure. # Simply bails out if CRC does not match. # def verifyFile(firmwareFile): print "Verifying file..." # read file header firmwareFile.seek(0, os.SEEK_SET) fileHeaderData = firmwareFile.read(FILE_HEADER_SIZE) fileHeader = FileHeader(unpack('_.FLC. Tables types are unknown # as are most systems. SD_K_C.FLC is the sensor info # table for a PowerEdge 2800. # @param Firmware object # @return list of PowerEdge objects found # def scanSystems(firmware): print "Scanning firmware file for available systems" # read header and store it print " - reading and parsing file header" firmware.ffile.seek(0, os.SEEK_SET) fileHeaderData = firmware.ffile.read(FILE_HEADER_SIZE) fileHeader = FileHeader(unpack(' 0 and blockHeader.btype == 11): print " > found sensor block!" m = re.match('^SD_(.*)\.FLC', blockHeader.filename) if (m): peSystem = PowerEdgeSystem() peSystem.sensorBlockId = blockId peSystem.sensorBlockHeader = blockHeader if (m.group(1) in systems): peSystem.unknown = False peSystem.idStr = systems[m.group(1)] print " > found a known system: %s" % peSystem.idStr else: peSystem.unknown = True peSystem.idStr = "unknown PowerEdge (code: %s)" % m.group(1) print " > found an unknown system." peSystem = scanSensorBlock(firmware, peSystem) peSystems.append(peSystem) return peSystems ######################################################## # Scans the sensor table for fans and stores the retrieved # info (number of fans, RPMs etc) in the PowerEdge object # @param Firmware object # @param PowerEdge object (selected system) # def scanSensorBlock(firmware, peSystem): firmware.ffile.seek(peSystem.sensorBlockHeader.offset, os.SEEK_SET) numInfos = unpack(' CRC ok" # read the sensor info data sensorInfoData = firmware.ffile.read(sensorInfoHeader.length) sensorInfoDataCRC16 = unpack(' found valid fan sensor info block: %s: %d/%d RPM" % (fanSensorInfoBlock.name, fanSensorInfoBlock.threshold, fanSpeedThreshold) peSystem.numFans += 1 peSystem.fanNames.append(fanSensorInfoBlock.name) peSystem.fanThresholds.append(fanSensorInfoBlock.threshold) peSystem.fanSpeeds.append(fanSpeedThreshold) peSystem.sensorNumbers.append(fanSensorInfoBlock.sensorId) infoIdx += 1 return peSystem ######################################################## # Write the fan sensor info from the PowerEdge object back into # the firmware file. # @param Firmware object # @param PowerEdge object (selected system) # def writeSensorBlock(firmware, peSystem): firmware.ffile.seek(peSystem.sensorBlockHeader.offset, os.SEEK_SET) numInfos = unpack(' CRC ok" # read the sensor info data sensorInfoData = firmware.ffile.read(sensorInfoHeader.length) sensorInfoDataCRC16 = unpack(' updating fan sensor with new threshold %d" % fanSensorInfoBlock.threshold firmware.ffile.seek(-(sensorInfoHeader.length+2), os.SEEK_CUR) sensorInfoData = pack(' computing and updating sensor block CRC 0x%04x" % computedCRC16 firmware.ffile.write(pack("\n" % sys.argv[0]) sys.exit(1) print "Opening file '%s'" % sys.argv[1] firmwareFile = open(sys.argv[1], "r+") # read firmware header firmware = readFirmwareHeader(firmwareFile) # verify the file before we do anything verifyFile(firmwareFile) # read the data file and scan for available pe systems peSystems = scanSystems(firmware) # # Present found data to user and let hir select the system to patch # print "\nSystems found in firmware file:\n" for i in range(len(peSystems)): peSystem = peSystems[i] print " %2d) %s" % (i+1, peSystem.idStr) print " Number of fans: %d" % (peSystem.numFans) print " Fan names : %s" % (', '.join(peSystem.fanNames)) print " Fan speeds : %s" % (str(peSystem.fanSpeeds).strip('[]')) print " Sensor numbers: %s" % (str(peSystem.sensorNumbers).strip('[]')) print i += 1 num = 0 while (num <= 0 or num > i): sys.stdout.write("Select (1-%d): " % i) try: num = int(raw_input()) except ValueError: print "Oops! That was no valid number. Try again..." peSystem = peSystems[num-1] print "You selected the following system: %s\n" % (peSystem.idStr) print " Number of fans: %d" % (peSystem.numFans) print " Fan names : %s" % (', '.join(peSystem.fanNames)) print " Fan speeds : %s" % (str(peSystem.fanSpeeds).strip('[]')) print " Sensor numbers: %s" % (str(peSystem.sensorNumbers).strip('[]')) print # # Present all fan sensors for the selected system and let hir # select sensor to change, write file or bail out # while (type(num) is not str): print "Select fan to adjust:\n" for i in range(peSystem.numFans): print " %2d) fan sensor number %2d, threshold %4d, name '%s'" % (i+1, peSystem.sensorNumbers[i], peSystem.fanSpeeds[i], peSystem.fanNames[i]) print print " w) write sensor thresholds to firmware" print " x) quit without any changes" print i += 1 num = 0 while (num <= 0 or num > i): sys.stdout.write("Select (1-%d,w): " % i) num = raw_input() if (num.lower() == 'w' or num.lower() == 'x'): break; try: num = int(num) except ValueError: print "Oops! That was no valid number. Try again..." if (type(num) is str): break; num -= 1 print print "Editing threshold for fan number %d (%s)" % (peSystem.sensorNumbers[num], peSystem.fanNames[num]) print "Value will be multipled with %d to give actual RPM value" % IMPI_VALUE_MULTIPLIER print "Current value is %d (= %d RPM)" % (peSystem.fanThresholds[num], peSystem.fanSpeeds[num]) print "Enter new value: " value = -1 while (value < 0 or value > 255): sys.stdout.write("Select (0-255): ") try: value = int(raw_input()) except ValueError: print "Oops! That was no valid number. Try again..." peSystem.fanThresholds[num] = value peSystem.fanSpeeds[num] = value * IMPI_VALUE_MULTIPLIER if (type(num) is not str or num.lower() == 'x'): sys.exit(1) # write block writeSensorBlock(firmware, peSystem) # recompute CRC update header updateBlockCRC(firmware, peSystem) # recompute total CRC update header updateFileCRC(firmware, peSystem) # verify block verifyBlock(firmware, peSystem.sensorBlockId) # verify file verifyFile(firmwareFile) print print "All done." print print "**Disclaimer **" print print "If you flash this firmware, you might render your PowerEdge server unusable." print "It might even be unrecoverable. Additionally, badly set thresholds might cause" print "overheating." print print "I am not responsible for any damage that is done to your system." print print " YOU HAVE BEEN WARNED!" print print "Nevertheless, this patch worked fine for me." print "If it does work for you as well, please leave some feedback:" print "http://projects.nuschkys.net/projects/dell-poweredge-2800/" print if (peSystem.unknown): print "Please also report this string '"+peSystem.idStr+"'" print "along with your type of PowerEdge! Thank you." print sys.exit(0)