Dell PowerEdge R730 默认的散热策略(iDRAC 根据 CPU/内存温度自动调速)在大多数场景下都够用,但在家用、低负载或改造机房环境下,风扇转速依然偏高,噪音感人。这台机器用 ipmitool 走 iDRAC 的 IPMI raw 命令可以把手动控制权拿回来。
这篇文章介绍一个我写的风扇手动控制脚本(fanctl.sh)的原理和使用方法,脚本源码在文末一并给出。
为什么能控制?
PowerEdge 服务器的 iDRAC 内置了 IPMI(Intelligent Platform Management Interface),它提供了一套与操作系统无关的带外管理通道,通过 IPMI Over LAN(lanplus 接口)即可在局域网内直接给 BMC 下发指令。iDRAC 开放了一组 raw 命令(直接封装 IPMI 厂商私有 OEM 命令),其中就包含风扇控制的三条关键指令:
| 用途 | raw 命令 | 说明 |
|---|---|---|
| 切换手动模式 | raw 0x30 0x30 0x01 0x00 |
最后的 0x00 表示关闭自动调速,接管风扇 |
| 恢复自动模式 | raw 0x30 0x30 0x01 0x01 |
0x01 表示交还给 BIOS/iDRAC 自动管理 |
| 设置转速 | raw 0x30 0x30 0x02 0xff 0xXX |
0xff 后跟一个字节,是 0-100 的转速百分比 |
所以整个脚本的核心逻辑只有三步:
- 通过
ipmitool -I lanplus连接 iDRAC; - 发送
0x30 0x30 0x01 0x00切到手动模式; - 发送
0x30 0x30 0x02 0xff 0xXX设置目标转速,0xXX是十六进制表示的百分比。
脚本做了什么
源码:下载 fanctl.sh(与文章展示完全一致)
1. 连接参数
IP="192.168.31.92"
USER="root"
PASS="calvin"
脚本顶部集中放置 iDRAC 的 IP、用户名和密码。calvin 是 Dell 服务器的出厂默认密码,首次使用记得改掉。
ipmi() 函数把所有调用统一收敛起来:
ipmi() {
ipmitool -I lanplus -H "$IP" -U "$USER" -P "$PASS" "$@"
}
之后所有指令都走这一个函数,改连接参数时只需动一处。
2. 百分比与十六进制的换算
IPMI raw 命令里转速是一个字节(0x00-0xFF),而用户更习惯输入百分比。脚本用 pct2hex 做转换:
pct2hex() {
local pct=$1
local hex
hex=$(printf '%x' "$pct")
if (( pct < 16 )); then
echo "0x0${hex}"
else
echo "0x${hex}"
fi
}
把十进制百分比转成两位十六进制:比如 35 变成 0x23,8 变成 0x08。反向的 hex2pct 目前主要用于对称展示。
3. 模式切换与调速
set_manual() { ipmi $MANUAL_CMD; } # 切手动
set_auto() { ipmi $AUTO_CMD; } # 恢复自动
set_speed() { ipmi $SPEED_CMD_PREFIX "$hex"; } # 设转速
set_speed 会先校验范围,非法输入(< 1 或 > 100)直接报错退出,防止把转速设成 0 或超过 100%:
if (( pct < 1 || pct > 100 )); then
_ speed_err
return 1
fi
4. 安全兜底:Ctrl+C 自动恢复
这是脚本最关键的设计。手动模式下风扇由你接管,如果脚本中途被 Ctrl+C 打断就退出,风扇会保持你最后设定的低转速,服务器可能因此在无保护的情况下过热。脚本用 trap 兜底:
toggle_manual_trap() {
_ trap_msg
set_auto
exit 0
}
trap toggle_manual_trap SIGINT SIGTERM
收到 SIGINT/SIGTERM 时,先恢复自动模式再退出。无论正常退出还是被中断,风扇控制权都回到 iDRAC 手里。
5. 中英双语输出
脚本根据 $LANG / $LANGUAGE 环境变量自动判断语言(包含 zh/cn 则中文,否则英文),并把所有提示文案集中在一个 _() 函数里做查表。交互模式中还支持在运行时用 lang zh / lang en 随时切换。
6. 两种使用方式
交互模式(不带参数直接运行):
./fanctl.sh
进入 fanctl> 提示符,支持:
- 输入
1-100的任意数字直接调速; auto恢复自动模式后重新进入手动,方便对比;status查看当前连接配置;lang zh|en切换语言;exit/quit/q退出,退出前自动恢复自动模式;- 任何时候
Ctrl+C也会恢复自动模式并退出。
单命令模式(带一个参数):
./fanctl.sh 35 # 一键把风扇调到 35%
./fanctl.sh auto # 恢复自动控制
./fanctl.sh status # 查看状态
./fanctl.sh help # 查看帮助
适合在开机脚本、cron 或一键脚本里调用,比如开机降噪可以直接写 fanctl.sh 25。
使用前提
- 安装
ipmitool(Arch:pacman -S ipmitool); - 能通过局域网访问 iDRAC 的 IPMI 端口(623/UDP);
- 操作系统上装了 ipmitool 的驱动/内核模块(一般发行版自带)。
注意事项
- 风险自担:手动模式取消了温控保护,转速设太低在高温高负载下可能引发过热甚至关机。建议设置合理下限(比如 20% 以上),并留一只眼睛盯着温度。
- 脚本启动时会自动先切到手动模式,退出时保证恢复自动——这是它与其他一次性命令最大的区别。
- iDRAC 的
raw 命令属于厂商私有 OEM 指令,不同代际/型号可能不一致,本脚本针对 R730(iDRAC8)验证。 - 默认密码
calvin如果没改,意味着局域网内任何机器都能控制你的风扇(甚至远程开关机),务必先改密。
完整源码
#!/bin/bash
set -e
IP="192.168.31.92"
USER="root"
PASS="calvin"
MANUAL_CMD="raw 0x30 0x30 0x01 0x00"
AUTO_CMD="raw 0x30 0x30 0x01 0x01"
SPEED_CMD_PREFIX="raw 0x30 0x30 0x02 0xff"
detect_lang() {
local lang="${LANG:-}${LANGUAGE:-}"
if [[ "$lang" == *zh* || "$lang" == *ZH* || "$lang" == *cn* || "$lang" == *CN* ]]; then
CURRENT_LANG="zh"
else
CURRENT_LANG="en"
fi
}
detect_lang
_() {
local key="$1"; shift
case "$key" in
test_conn) [[ "$CURRENT_LANG" == "zh" ]] && echo -n "正在测试 IPMI 连接到 ${IP}... " || echo -n "Testing IPMI connection to ${IP}... " ;;
ok) [[ "$CURRENT_LANG" == "zh" ]] && echo "成功" || echo "OK" ;;
failed) [[ "$CURRENT_LANG" == "zh" ]] && echo "失败" || echo "FAILED" ;;
manual_on) [[ "$CURRENT_LANG" == "zh" ]] && echo -n "切换到手动风扇控制... " || echo -n "Switching to manual fan control... " ;;
auto_on) [[ "$CURRENT_LANG" == "zh" ]] && echo -n "恢复到自动风扇控制... " || echo -n "Restoring automatic fan control... " ;;
done) [[ "$CURRENT_LANG" == "zh" ]] && echo "完成" || echo "Done" ;;
speed_err) [[ "$CURRENT_LANG" == "zh" ]] && echo "错误: 百分比必须在 1-100 之间" || echo "ERROR: percentage must be 1-100" ;;
set_speed) [[ "$CURRENT_LANG" == "zh" ]] && echo -n "设置风扇速度为 $1% ($2)... " || echo -n "Setting fan speed to $1% ($2)... " ;;
re_manual) [[ "$CURRENT_LANG" == "zh" ]] && echo "重新启用手动模式..." || echo "Re-enabling manual mode..." ;;
trap_msg) [[ "$CURRENT_LANG" == "zh" ]] && echo "检测到中断信号,恢复自动模式并退出..." || echo "Caught interrupt, restoring auto mode and exiting..." ;;
exit_msg) [[ "$CURRENT_LANG" == "zh" ]] && echo "恢复自动模式并退出..." || echo "Restoring auto mode and exiting..." ;;
title) [[ "$CURRENT_LANG" == "zh" ]] && echo "IPMI 风扇控制 - 交互模式" || echo "IPMI Fan Control - Interactive Mode" ;;
info) [[ "$CURRENT_LANG" == "zh" ]] && echo "Power By K-Black" || echo "Power By K-Black" ;;
prompt_text) [[ "$CURRENT_LANG" == "zh" ]] && echo "输入风扇速度 (1-100), 'auto', 'status', 'exit', 'lang zh|en'" || echo "Enter fan speed (1-100), 'auto', 'status', 'exit', 'lang zh|en'" ;;
ctrlc_note) [[ "$CURRENT_LANG" == "zh" ]] && echo "Ctrl+C 恢复自动模式并退出。" || echo "Ctrl+C restores auto and exits." ;;
invalid) [[ "$CURRENT_LANG" == "zh" ]] && echo "无效输入: 请输入 1-100, 'auto', 'status', 'exit', 'lang zh|en'" || echo "Invalid input: enter 1-100, 'auto', 'status', 'exit', 'lang zh|en'" ;;
lang_sw) [[ "$CURRENT_LANG" == "zh" ]] && echo "语言已切换为: 中文" || echo "Language switched to: English" ;;
lang_usage) [[ "$CURRENT_LANG" == "zh" ]] && echo "用法: lang zh|en 当前语言: ${CURRENT_LANG}" || echo "Usage: lang zh|en Current: ${CURRENT_LANG}" ;;
status_header) [[ "$CURRENT_LANG" == "zh" ]] && echo "--- 当前状态 ---" || echo "--- Current Status ---" ;;
status_ip) [[ "$CURRENT_LANG" == "zh" ]] && echo "IP: ${IP}" || echo "IP: ${IP}" ;;
status_user) [[ "$CURRENT_LANG" == "zh" ]] && echo "用户: ${USER}" || echo "User: ${USER}" ;;
status_mode) [[ "$CURRENT_LANG" == "zh" ]] && echo "模式: 手动" || echo "Mode: manual" ;;
help_usage) [[ "$CURRENT_LANG" == "zh" ]] && echo "用法:" || echo "Usage:" ;;
help_fan) [[ "$CURRENT_LANG" == "zh" ]] && echo " fan 按百分比设置速度 (1-100),自动转换为十六进制" || echo " fan set speed by percentage (1-100), converts to hex automatically" ;;
help_auto) [[ "$CURRENT_LANG" == "zh" ]] && echo " auto 恢复自动风扇控制" || echo " auto restore automatic fan control" ;;
help_status) [[ "$CURRENT_LANG" == "zh" ]] && echo " status 显示当前设置" || echo " status show current settings" ;;
help_lang) [[ "$CURRENT_LANG" == "zh" ]] && echo " lang zh|en 切换语言" || echo " lang zh|en switch language" ;;
help_exit) [[ "$CURRENT_LANG" == "zh" ]] && echo " exit / quit / q 退出脚本" || echo " exit / quit / q exit script" ;;
separator) echo "===================================" ;;
esac
}
ipmi() {
ipmitool -I lanplus -H "$IP" -U "$USER" -P "$PASS" "$@"
}
pct2hex() {
local pct=$1
local hex
hex=$(printf '%x' "$pct")
if (( pct < 16 )); then
echo "0x0${hex}"
else
echo "0x${hex}"
fi
}
hex2pct() {
local hex=$1
printf '%d' "$hex"
}
test_conn() {
_ test_conn
if ipmi mc info &>/dev/null; then
_ ok
else
_ failed
exit 1
fi
}
set_manual() {
_ manual_on
ipmi $MANUAL_CMD
_ done
}
set_auto() {
_ auto_on
ipmi $AUTO_CMD
_ done
}
set_speed() {
local pct=$1
if (( pct < 1 || pct > 100 )); then
_ speed_err
return 1
fi
local hex
hex=$(pct2hex "$pct")
_ set_speed "$pct" "$hex"
ipmi $SPEED_CMD_PREFIX "$hex"
_ done
}
cmd_status() {
_ status_header
_ status_ip
_ status_user
_ status_mode
}
cmd_help() {
_ help_usage
_ help_fan
_ help_auto
_ help_status
_ help_lang
_ help_exit
}
toggle_manual_trap() {
echo ""
_ trap_msg
set_auto
exit 0
}
trap toggle_manual_trap SIGINT SIGTERM
test_conn
set_manual
run_cmd() {
case "$1" in
auto)
set_auto
;;
status)
cmd_status
;;
help|'?'|h|--help|-h)
cmd_help
echo ""
_ prompt_text
exit 0
;;
lang|lang\ *)
arg="${1#lang}"
arg="${arg# }"
case "$arg" in
zh|cn|中文) CURRENT_LANG="zh" ;;
en) CURRENT_LANG="en" ;;
esac
;;
''|*[!0-9]*)
_ invalid
exit 1
;;
*)
set_speed "$1"
;;
esac
}
if [[ $# -gt 0 ]]; then
test_conn
trap toggle_manual_trap SIGINT SIGTERM
run_cmd "$1"
exit 0
fi
echo ""
_ title
_ separator
_ info
_ separator
cmd_help
echo ""
_ ctrlc_note
echo ""
while true; do
read -r -p "fanctl> " input
case "$input" in
exit|quit|q)
_ exit_msg
set_auto
exit 0
;;
auto)
set_auto
_ re_manual
set_manual
;;
status)
cmd_status
;;
help|'?'|h)
cmd_help
;;
lang|lang\ *)
arg="${input#lang}"
arg="${arg# }"
case "$arg" in
zh|cn|中文)
CURRENT_LANG="zh"
_ lang_sw
;;
en)
CURRENT_LANG="en"
_ lang_sw
;;
'')
_ lang_usage
;;
*)
_ lang_usage
;;
esac
;;
''|*[!0-9]*)
_ invalid
;;
*)
set_speed "$input"
;;
esac
done
下载源码:fanctl.sh