Skip to content

Instantly share code, notes, and snippets.

@eatnumber1
Created September 12, 2024 19:12
Show Gist options
  • Save eatnumber1/0734f4e006c2cd1122df9e0ffc34507f to your computer and use it in GitHub Desktop.
Save eatnumber1/0734f4e006c2cd1122df9e0ffc34507f to your computer and use it in GitHub Desktop.
jbod-fancontrol - Control fans on Supermicro JBODs via the out-of-band SMCIPMITool
#!/bin/zsh
# Author: Russell Harmon
setopt err_exit err_return
setopt function_argzero no_unset warn_create_global
declare -a flag_help flag_idle_percent_mode flag_user flag_password flag_fan_speed_percent flag_verbose
zparseopts -D -F -K -- \
{h,-help}=flag_help \
-idle_percent_mode=flag_idle_percent_mode \
{u,-user}:=flag_user \
{p,-password}:=flag_password \
{s,-fan_speed_percent}:=flag_fan_speed_percent \
{v,-verbose}=flag_verbose \
if (( $#flag_help )) || [[ $# -ne 1 ]]; then
cat <<- EOF
Usage: $0 [--idle_percent_mode] [-u|--user=<username>] [-p|--password=<password>] [-s|--fan_speed_percent=<speed>] [-v|--verbose] <hostname>
Configure a JBOD for a given fan speed.
Args:
hostname - The hostname of the JBOD to connect to.
Options:
-h, --help - Print the help message
-v, --verbose - Log what's being done
-u, --user <username> - The username to log in as
-p, --password <password> - The password to log in as
-s, --fan_speed_percent <speed>
The fan speed to set, as measured in percent values 0-100%.
--idle_percent_mode - Inputs to the JBOD are measured in percent idle.
EOF
exit 0
fi
host=$1
user=${flag_user[-1]}
pass=${flag_password[-1]}
declare -i desired_speed
desired_speed=1
if (( $#flag_fan_speed_percent )); then
desired_speed=${flag_fan_speed_percent[-1]}
fi
#ip=$(dig +short $host)
ip=$host
if [[ -z $ip ]]; then
echo "Unknown host $host" >&2
exit 1
fi
# This script is based on notes from
#
# https://forums.servethehome.com/index.php?resources/supermicro-x9-x10-x11-fan-speed-control.20/
#
# It uses the SMCIPMITool subcommand:
#
# ipmi raw <netFn> <cmd> [data]
function SMCIPMITool {
setopt local_options
if (( $#flag_verbose )); then
set -x
fi
command SMCIPMITool "$@"
}
function smc {
local lines
local -i rc
lines="$(SMCIPMITool "$ip" "$user" "$pass" "$@")" || rc=$?
if [[ $rc -ne 0 ]]; then
echo "Failure: SMCIPMITool [...] $*" >&2
echo "$lines" >&2
return $rc
fi
echo "$lines"
}
function ipmi {
smc ipmi "$@"
}
# Valid values for cmd 45
declare -rA fan_modes=(
'standard' 0
'full' 1
'optimal' 2
'heavy_io' 4
)
function get_fan_mode {
local -a lines
() {
local IFS=$'\n'
# netFn 30 is fan commands.
# cmd 45 controls the fan setting
# subcommand 00 returns the current fan setting
lines=( $(ipmi raw 30 45 00) )
}
if [[ ${lines[1]} != "00" ]]; then
echo "Unknown response from ipmi tool when getting fan mode: '${(j:\n:)lines[*]}'" >&2
return 1
fi
# Convert to an int to remove leading zeros
local -i code
code=${lines[2]}
local mode
mode=${(k)fan_modes[(r)${code}]}
if [[ -z $mode ]]; then
echo "Unknown fan mode: '$code'" >&2
return 1
fi
echo $mode
}
# Usage: set_fan_mode <mode>
function set_fan_mode {
local mode
mode=${fan_modes[$1]}
if [[ -z $mode ]]; then
echo "Unknown fan mode $1" >&2
return 1
fi
local mode_hex
mode_hex="$(printf '%02x' $mode)"
local lines
# netFn 30 is fan commands.
# cmd 45 controls the fan setting
# subcommand 01 sets the current fan setting
# the last arg determins which fan setting to set to
lines="$(ipmi raw 30 45 01 $mode_hex)"
if [[ "$lines" != "00" ]]; then
echo "Unknown response from ipmi tool when setting fan mode: '$lines'" >&2
return 1
fi
}
# Map a speed profile as measured in percent time busy in the range 0-100 (bigger is faster)
# to a value in percent time idle (bigger is slower) in the range 0-100.
function speed_busy_to_idle_percent {
declare -i busy_percent="$1"
echo $(( 100 - busy_percent ))
}
function speed_idle_to_busy_percent {
declare -i idle_percent="$1"
echo $(( 100 - idle_percent ))
}
# We can set the percent duty cycle with Cmd 70
# The usage is:
# ipmi raw 30 70 66 01 <zone> <duty_cycle>
# Note: We don't know what 66 means.
# Note: 66 00 is to read
# Note: 66 01 is to write
#
# The zones are 00 or 01. On jbod60, 00 seems to control all the fans.
# The duty cycle is 00 to FF (min to max).
# get_fan_speed
# prints the fan speed as 1-100%
function get_fan_speed {
local -a lines
() {
local IFS=$'\n'
lines=( $(ipmi raw 30 70 66 00) )
}
if [[ ${lines[1]} != "00" ]]; then
echo "Unknown response from ipmi tool when getting fan speed: '${(j:\n:)lines[*]}'" >&2
return 1
fi
# Convert to an int to remove leading zeros
speed="$(( 16#${lines[2]} ))"
if (( $#flag_idle_percent_mode )); then
speed="$(speed_idle_to_busy_percent $speed)"
fi
echo $speed
}
# set_fan_speed <speed>
# speed - A value 0-100% busy
function set_fan_speed {
local speed=$1
if (( speed > 100 || speed < 1 )); then
echo "Invalid fan speed $speed" >&2
return 1
fi
if (( $#flag_idle_percent_mode )); then
speed="$(speed_busy_to_idle_percent $speed)"
fi
local speed_hex
speed_hex="$(printf '%02x' $speed)"
local lines
lines="$(ipmi raw 30 70 66 01 00 $speed_hex)"
if [[ "$lines" != "00" ]]; then
echo "Unknown response from ipmi tool when setting fan speed: '$lines'" >&2
return 1
fi
}
function is_powered_on {
local lines
lines=$(ipmi power status)
if [[ $lines = "Power is currently on." ]]; then
return 0
elif [[ $lines = "Power is currently off." ]]; then
return 1
else
echo "Unknown response from ipmi tool when checking power status: '$lines'" >&2
exit 1
fi
}
if ! is_powered_on; then
exit 0
fi
local mode
mode="$(get_fan_mode)"
if [[ $mode != full ]]; then
(( $#flag_verbose )) && echo "Fan mode is $mode. Setting to 'full'."
set_fan_mode full
fi
local speed
speed="$(get_fan_speed)"
if [[ $speed -ne $desired_speed ]]; then
(( $#flag_verbose )) && echo "Fan speed is $speed. Setting to $desired_speed."
set_fan_speed $desired_speed
fi
# vim:noet
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment