#!/bin/bash # Temperature-aware IPMI fan control for Dell PowerEdge / iDRAC. # # Holds the fans at a fixed low duty while the CPUs are cool, and hands control # back to the BMC's automatic curve once they get hot, so there is still a # thermal response under load. This is the native equivalent of the # tigerblue77/dell_idrac_fan_controller container, without needing Docker. # # Run under ipmi-fancontrol.service. Configuration lives in # conf/ipmi-fancontrol.conf (see conf/ipmi-fancontrol.conf.example). set -uo pipefail SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) CONFIG=${IPMI_FANCONTROL_CONFIG:-$SCRIPT_DIR/conf/ipmi-fancontrol.conf} if [[ ! -r "$CONFIG" ]]; then echo "Error: cannot read $CONFIG" >&2 echo "Copy conf/ipmi-fancontrol.conf.example there and fill it in." >&2 exit 1 fi # shellcheck source=/dev/null . "$CONFIG" : "${IPMI_INTERFACE:=open}" : "${FAN_SPEED:=0x15}" : "${CPU_TEMP_THRESHOLD:=70}" : "${CPU_TEMP_HYSTERESIS:=5}" : "${CHECK_INTERVAL:=60}" : "${DISABLE_THIRD_PARTY_PCIE_COOLING_RESPONSE:=false}" RESUME_AT=$((CPU_TEMP_THRESHOLD - CPU_TEMP_HYSTERESIS)) # Build the ipmitool invocation once. The local "open" interface needs no # credentials and no network, so it is the better choice for a polling loop; # lanplus stays available for reaching a BMC over the network. if [[ "$IPMI_INTERFACE" == "lanplus" ]]; then : "${IPMI_HOST:?not set in $CONFIG}" : "${IPMI_USER:?not set in $CONFIG}" : "${IPMI_PASSWORD:?not set in $CONFIG}" export IPMI_PASSWORD IPMITOOL=(ipmitool -I lanplus -H "$IPMI_HOST" -U "$IPMI_USER" -E) else IPMITOOL=(ipmitool -I open) fi # stdout goes to the journal under systemd. log() { echo "$*"; } set_manual() { "${IPMITOOL[@]}" raw 0x30 0x30 0x01 0x00 >/dev/null && "${IPMITOOL[@]}" raw 0x30 0x30 0x02 0xff "$FAN_SPEED" >/dev/null } set_auto() { "${IPMITOOL[@]}" raw 0x30 0x30 0x01 0x01 >/dev/null } # Highest CPU temperature in degrees C, or empty if none could be read. # CPU sensors are entity 3.x; 7.x is the system board (inlet/exhaust). max_cpu_temp() { "${IPMITOOL[@]}" sdr type temperature 2>/dev/null | awk -F'|' ' $4 ~ /^[[:space:]]*3\./ { if (match($5, /-?[0-9]+/)) { t = substr($5, RSTART, RLENGTH) + 0 if (!seen || t > max) max = t seen = 1 } } END { if (seen) print max } ' } MODE="" cleanup() { log "Stopping: returning fan control to the BMC automatic curve." set_auto || log "Warning: failed to restore automatic fan control." exit 0 } trap cleanup TERM INT if [[ "$DISABLE_THIRD_PARTY_PCIE_COOLING_RESPONSE" == "true" ]]; then if "${IPMITOOL[@]}" raw 0x30 0xce 0x00 0x16 0x05 0x00 0x00 0x00 0x05 0x00 0x01 0x00 0x00 >/dev/null 2>&1; then log "Disabled the third-party PCIe card default cooling response." else log "Warning: could not disable the third-party PCIe cooling response." fi fi log "Starting: fans held at $FAN_SPEED below ${CPU_TEMP_THRESHOLD}C, automatic at or above it, re-engaging at ${RESUME_AT}C. Polling every ${CHECK_INTERVAL}s over $IPMI_INTERFACE." while true; do temp=$(max_cpu_temp) if [[ -z "$temp" ]]; then # Fail safe: if the temperature cannot be read, let the BMC cool the box. if [[ "$MODE" != "auto" ]]; then log "Cannot read CPU temperature; failing safe to automatic fan control." set_auto && MODE="auto" fi elif (( temp >= CPU_TEMP_THRESHOLD )); then if [[ "$MODE" != "auto" ]]; then log "CPU at ${temp}C, at or above ${CPU_TEMP_THRESHOLD}C: handing control to the BMC automatic curve." set_auto && MODE="auto" fi elif [[ -z "$MODE" ]] || (( temp <= RESUME_AT )); then # The -z test makes the very first pass decide rather than drift, even # if we start up inside the hysteresis band. if [[ "$MODE" != "manual" ]]; then log "CPU at ${temp}C, at or below ${RESUME_AT}C: holding fans at $FAN_SPEED." set_manual && MODE="manual" fi fi # Inside the hysteresis band we deliberately keep whatever mode we are in. # Backgrounded so that SIGTERM interrupts the wait instead of being deferred # until the sleep finishes. sleep "$CHECK_INTERVAL" & wait $! done