diff --git a/README.md b/README.md index 7302c89..22be1c4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ hopefully all Supermicro X8 / X9 / X10 / X11 boards with IPMI. I have personally #### Hardware * Supermicro X9DRi-LN4+ * Supermicro X8SIL-F (IPMI equipped variant) +* Supermicro X9SRW-F (IPMI equipped variant, using external ipmitool from OS repos) #### Operating Systems * VMWare ESXi 6.7 @@ -25,6 +26,7 @@ hopefully all Supermicro X8 / X9 / X10 / X11 boards with IPMI. I have personally * Ubuntu 20.04 * Ubuntu 18.04 * Ubuntu 16.04 +* Proxmox VE 8.4 / Debian 12 "Bookworm" For this script to work, you **MUST** have an IPMI module **AND** set your fan speeds to FULL SPEED in the BIOS otherwise this tool fights for control with the fans and they will spin up and down repeatedly (yo-yo'ing). @@ -63,6 +65,23 @@ Finally, run `crontab -e` and add the following line; @reboot /opt/FanControl/daemon.sh ~~~ +#### Proxmox / Debian +On Proxmox systems, I would suggest installing the ipmitool package from the operating system's repository: + +~~~ +sudo apt-get install ipmitool +~~~ + +Then, similar to Ubuntu instructions above, find a suitable place for the scripts to live such as `/opt/FanControl/`. Personally I preferred `/opt/Supermicro-Fan-Control` because then I could just: +* login as root +* install git: `apt-get install git` +* change directory to `/opt/`: `cd /opt` +* clone the repository: `git clone 'https://github.com/jasongaunt/Supermicro-Fan-Control.git'` +* change directory to `/opt/Supermicro-Fan-Control`: `cd Supermicro-Fan-Control` +* edit the configuration file as needed: `nano config.ini`, ensuring the IPMITOOL is set properly + +Finally, run `crontab -e` as under Ubuntu, or use another method to load and run it on startup. + Hardware fan assignments ------------------------ @@ -84,6 +103,8 @@ Supermicro assume Zone A is used for cooling the main system and Zone B for cool * Zone A = Anything that cools the CPU, memory and motherboard * Zone B = Anything that cools PCIe cards and / or drive bays +For at least one Supermicro X9SRW-F board in a 2U case, it seemed these were reversed, so YMMV. + #### Desktop cases If, like myself, you're using a Supermicro board in a desktop case, chances are you've already using dedicated CPU coolers with their @@ -166,3 +187,7 @@ PWM% The values included in `config.ini` by default are sane values to start with. Good luck! ~ JG + +#### Hushing Loud Server Fans + +The fans on my X9SRW-F 2U server were extremely loud by default, even with no load and CPU at 30C. I set 1% fan speed with ~25C/77F ambient room temperature causing my low TDP CPU to idle at 31C with the fans almost silent. In this same configuration with 1% fan speed, my CPU only got up to around 45C at full load. To be safe, I made sure to set my minimum temperature to 33C, and kept my 100% speed threshold far below the CPU thermal limits, but YMMV. -BG diff --git a/config.ini b/config.ini index d11a79f..9ce650a 100644 --- a/config.ini +++ b/config.ini @@ -33,3 +33,4 @@ Temp Averaging Window = 5 Ignore Temp Change Amount = 0 Exit On IPMI Failure = False Debug Mode = False +IPMITOOL = ipmitool diff --git a/fan-control.py b/fan-control.py index e4c7e43..05fc550 100755 --- a/fan-control.py +++ b/fan-control.py @@ -14,6 +14,7 @@ # Import required modules import os, sys, re, time, configparser, statistics from subprocess import Popen, PIPE +import shutil # Set up our default variables with safe values ZONE_A_SENSOR_NAME_SEARCH = r'^.*CPU.*$' @@ -32,10 +33,12 @@ IGNORE_TEMP_CHANGE_AMOUNT = 1 EXIT_ON_FAILURE = False DEBUG = False +IPMITOOL = False # Wrapper for (re)reading config.ini def reload_config(): global DEBUG + global IPMITOOL if DEBUG: sys.stdout.write('Reloading config... '); sys.stdout.flush() config = configparser.ConfigParser() config.read(os.path.join(os.path.dirname(__file__), './config.ini')) @@ -59,10 +62,61 @@ def reload_config(): global EXIT_ON_FAILURE; EXIT_ON_FAILURE = config.get('General Configuration', 'Exit On IPMI Failure').lower() in ["yes", "true", "1"] DEBUG = config.get('General Configuration', 'Debug Mode').lower() in ["yes", "true", "1"] + global IPMITOOL; + ipmitool_bin = None + try: + IPMITOOL = config.get('General Configuration', 'IPMITOOL') + if IPMITOOL.lower() in [ "", "0", "false", "none" ]: + IPMITOOL = False + else: + # validate the external command exists, or replace with False + ipmitool_bin = shutil.which(IPMITOOL) + if ipmitool_bin is None: + sys.stdout.write("\n\nError: Unable to find ipmitool in system path: " + IPMITOOL + "\n") + IPMITOOL = False + except configparser.NoOptionError as e: + if DEBUG: sys.stdout.write("\n" + str(e) + "\n") + IPMITOOL = False + # if false, use the builtin IPMICFG tool + if IPMITOOL == False: + pass + elif not os.path.isfile(ipmitool_bin): + err = "Unable to find external IPMITOOL at: " + ipmitool_bin + sys.stdout.write("\n\nError: " + err + "\n") + IPMITOOL = False + elif not os.access(ipmitool_bin, os.X_OK): + err = "Unable to execute external IPMITOOL at: " + ipmitool_bin + sys.stdout.write("\n\nError: " + err + "\n") + IPMITOOL = False + if DEBUG and IPMITOOL: + sys.stdout.write("\nUsing ipmitool: " + IPMITOOL + "\n") + if DEBUG: sys.stdout.write("done\n") +# call external tool for making IPMI calls +def call_ipmitool(params): + global IPMITOOL + IPMICWD = os.path.dirname(__file__) + if params[0] in [ "-raw", "-sdr" ]: + params[0] = params[0][1:] + else: + if DEBUG: + sys.stdout.write('Unknown params[0]: ' + params[0] + '\n') + sys.stdout.flush() + err = "Error: Unknown argument in call to external ipmitool: " + params[0] + return [-1, '', err] + IPMICMD = [IPMITOOL] + params + if DEBUG: sys.stdout.write(' ' + ' '.join(IPMICMD) + '\n') + process = Popen(IPMICMD, stdout=PIPE, cwd=IPMICWD) + (output, err) = process.communicate() + EXITCODE = process.wait() + if DEBUG: sys.stdout.write("IPMITOOL exit code: %d\n" % EXITCODE) + return [EXITCODE, output.decode('utf-8'), err] + # Wrapper for making IPMI calls def call_ipmi(params): + global IPMITOOL + if IPMITOOL: return call_ipmitool(params) IPMICMD = "./IPMICFG-Linux.x86" IPMICWD = os.path.join(os.path.dirname(__file__), "./ipmitool/") IPMICMD = [IPMICMD] + params @@ -141,11 +195,34 @@ def calculate_pwm(PEAK_TEMP, MIN_TEMP, MAX_TEMP, MIN_FAN_PWM, MAX_FAN_PWM): line[0] = line[0].strip() line[1] = line[1].strip() line[2] = line[2].strip() + # external ipmitool has a different sdr output format + if IPMITOOL: + temp = line[2] + line[2] = line[1] + line[1] = line[0] + line[0] = temp + del temp if DEBUG: sys.stdout.write(line[1] + ": " + line[2] + "\n"); sys.stdout.flush() # Check to see if we have a failed fan if ((line[0].lower() == "fail") and ("fan" in line[1].lower())): FAILED_FAN = True + # convert alternate temperature format to expected format + match = re.match(r'^(\d+) degrees (C|F)$', line[2]) + if match: + if match.group(2) == 'F': + # convert from Fahrenheit to Celsius + celsius = (int(match.group(1)) - 32) * 5 // 9 + line[2] = str(celsius) + "C/" + match.group(1) + "F" + #sys.stdout.write("F alternate format: " + line[1] + ": " + line[2] + "\n") + elif match.group(2) == 'C': + fahrenheit = (int(match.group(1)) * 9 // 5) + 32 + line[2] = match.group(1) + "C/" + str(fahrenheit) + "F" + #sys.stdout.write("C alternate format: " + line[1] + ": " + line[2] + "\n") + elif DEBUG: + sys.stdout.write("Error: unknown unit in alternate format: " + line[1] + ": " + line[2] + "\n") + del match + # Only continue past this point of the for-loop if we have a temperature value if not re.match(r'\d+C\/\d+F', line[2]): continue