Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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
* VMware ESXi 5.5
* 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).
Expand Down Expand Up @@ -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
------------------------
Expand All @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ Temp Averaging Window = 5
Ignore Temp Change Amount = 0
Exit On IPMI Failure = False
Debug Mode = False
IPMITOOL = ipmitool
77 changes: 77 additions & 0 deletions fan-control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.*$'
Expand All @@ -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'))
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down