服务器运维笔记(一)——使用shell编写实时查看BMC功率的脚本

1. 查看一次服务器BMC功率的脚本代码

利用shell脚本查看服务器的BMC的功率首先要下载ipmitool,在Ubuntu/Debian上下载ipmitool相关代码的指令为:

bash 复制代码
sudo apt update
sudo apt install -y ipmitool

在CentOS 7下载ipmitool相关指令为:

bash 复制代码
sudo yum install -y ipmitool

创建一个获得BMC的脚本并编写:

bash 复制代码
vim Get_BMC.sh

脚本内容(使用AI生成)为:

bash 复制代码
#!/usr/bin/env bash
# Get_BMC.sh
# 通过 BMC/IPMI 获取服务器整机瞬时功耗并打印
#
# 固定目标 BMC IP:××.××.××.××
# 固定用户名:××××××
# 固定密码:××××××

set -euo pipefail

BMC_HOST="××.××.××.××"
BMC_USER="×××××"
BMC_PASS='×××××'

for c in ipmitool timeout awk; do
    command -v "$c" >/dev/null 2>&1 || {
        echo "ERROR: 未找到 $c,请在普通 Linux 上安装相关工具" >&2
        exit 1
    }
done

# 可选 ipmitool 参数,例如某些 BMC 需要:IPMI_EXTRA="-C 17" ./Get_BMC.sh
IPMI_EXTRA_ARGS=()
if [[ -n "${IPMI_EXTRA:-}" ]]; then
    read -r -a IPMI_EXTRA_ARGS <<< "$IPMI_EXTRA"
fi

ipmi() {
    local -a cmd

    # -E 表示从环境变量 IPMI_PASSWORD 读取密码,避免密码出现在命令行参数里
    cmd=(timeout 15 ipmitool -I lanplus -H "$BMC_HOST" -U "$BMC_USER" -E)

    if ((${#IPMI_EXTRA_ARGS[@]})); then
        cmd+=("${IPMI_EXTRA_ARGS[@]}")
    fi

    cmd+=("$@")

    IPMI_PASSWORD="$BMC_PASS" "${cmd[@]}"
}

echo "正在读取 BMC(${BMC_HOST}) 功耗..."

# 方式 1:优先使用 DCMI 标准功耗读数
if dcmi_out="$(ipmi dcmi power reading 2>&1)"; then
    power="$(awk -F: '/Instantaneous power reading/ {
        v=$2
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
        print v
        exit
    }' <<< "$dcmi_out")"

    if [[ -n "$power" ]]; then
        echo "BMC power: $power"
        exit 0
    fi

    echo "WARN: dcmi 命令成功,但没有解析到 Instantaneous power reading。原始输出:" >&2
    echo "$dcmi_out" >&2
else
    echo "WARN: dcmi power reading 失败,尝试从 sensor 读取。原始输出:" >&2
    echo "$dcmi_out" >&2
fi

完成脚本修改该脚本权限:

bash 复制代码
chmod 700 Get_BMC.sh

后可直接执行。

2. 每隔10s查看一次服务器其BMC的脚本功率的代码

在上面的代码的基础上,每10s查看服务器BMC脚本:

bash 复制代码
#!/usr/bin/env bash
# Get_BMC.sh
# 每 10 秒通过 BMC/IPMI 获取一次服务器整机瞬时功耗并打印
#
# 固定目标 BMC IP:××.××.××.××
# 固定用户名:×××××
# 固定密码:×××××

set -euo pipefail

BMC_HOST="××.××.××.××"
BMC_USER="×××××"
BMC_PASS='×××××'

# 采集间隔,默认 10 秒
INTERVAL="${INTERVAL:-10}"

for c in ipmitool timeout awk date sleep; do
    command -v "$c" >/dev/null 2>&1 || {
        echo "ERROR: 未找到 $c,请在普通 Linux 上安装相关工具" >&2
        exit 1
    }
done

# 可选 ipmitool 参数,例如:IPMI_EXTRA="-C 17" ./Get_BMC.sh
IPMI_EXTRA_ARGS=()
if [[ -n "${IPMI_EXTRA:-}" ]]; then
    read -r -a IPMI_EXTRA_ARGS <<< "$IPMI_EXTRA"
fi

ipmi() {
    local -a cmd

    # -E 表示从环境变量 IPMI_PASSWORD 读取密码,避免密码出现在命令行参数里
    cmd=(timeout 15 ipmitool -I lanplus -H "$BMC_HOST" -U "$BMC_USER" -E)

    if ((${#IPMI_EXTRA_ARGS[@]})); then
        cmd+=("${IPMI_EXTRA_ARGS[@]}")
    fi

    cmd+=("$@")

    IPMI_PASSWORD="$BMC_PASS" "${cmd[@]}"
}

get_dcmi_power() {
    local out power

    if ! out="$(ipmi dcmi power reading 2>&1)"; then
        return 1
    fi

    if ! power="$(awk -F: '/Instantaneous power reading/ {
        v=$2
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
        print v
        exit
    }' <<< "$out")"; then
        return 1
    fi

    if [[ -z "$power" ]]; then
        return 1
    fi

    printf '%s\n' "$power"
}

get_sensor_power() {
    local out power

    if ! out="$(ipmi sensor 2>&1)"; then
        return 1
    fi

    if ! power="$(awk -F'|' '
        BEGIN { best=""; found=0 }
        function trim(s) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s }
        function number(s) {
            if (match(s, /[0-9]+([.][0-9]+)?/)) return substr(s, RSTART, RLENGTH)
            return ""
        }
        {
            name=trim($1); val=trim($2); unit=trim($3)
            n=number(val)
            low=tolower($0)
            u=tolower(unit)
            ln=tolower(name)

            is_watt=(u ~ /^(w|watt|watts)$/ || low ~ /watt/ || low ~ /[[:space:]]w[[:space:]]/)

            if (n != "" && is_watt) {
                unit_out=(u ~ /^(w|watt|watts)$/ ? unit : "W")
                line=n " " unit_out

                # 优先选择整机/总功耗类传感器
                if (ln ~ /system|total|power meter|server|pwr consumption|power consumption/) {
                    print line
                    found=1
                    exit
                }

                if (best == "") best=line
            }
        }
        END { if (!found && best != "") print best }
    ' <<< "$out")"; then
        return 1
    fi

    if [[ -z "$power" ]]; then
        return 1
    fi

    printf '%s (sensor fallback)\n' "$power"
}

read_power_once() {
    local power

    if power="$(get_dcmi_power)"; then
        printf '%s\n' "$power"
        return 0
    fi

    if power="$(get_sensor_power)"; then
        printf '%s\n' "$power"
        return 0
    fi

    return 1
}

trap 'echo; echo "已停止 BMC 功耗采集。"; exit 0' INT TERM

echo "开始每 ${INTERVAL} 秒读取一次 BMC(${BMC_HOST}) 功耗,按 Ctrl+C 停止。"

while true; do
    ts="$(date '+%Y-%m-%d %H:%M:%S')"

    if power="$(read_power_once)"; then
        printf '[%s] BMC(%s) power: %s\n' "$ts" "$BMC_HOST" "$power"
    else
        printf '[%s] ERROR: 读取 BMC(%s) 功耗失败\n' "$ts" "$BMC_HOST" >&2
    fi

    sleep "$INTERVAL"
done
EOF

修改该脚本的权限:

bash 复制代码
chmod 700 Get_BMC.sh

执行该脚本命令。

3.在飞书机器人上面的实时传输服务器BMC功率

在飞书机器人上能够实时传输服务器BMC功率。首先在飞书群内创建一个机器人实时查看BMC功率。

首先在飞书中创建群组,然后在群组中创建自定义机器人:

点击自定义机器人,对于机器人进行命名和描述功能:

点击添加会获得一个之后会有一个webhook地址:https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxx

通过shell脚本编写如下代码:

复制代码
cat > Get_BMC_Feishu.sh <<'EOF'
#!/usr/bin/env bash
# Get_BMC_Feishu.sh
# 每 10 秒读取 BMC 功耗,并输出到飞书群机器人
#
# 固定目标 BMC IP:××.××.××.××
# 固定用户名:×××××
# 固定密码:×××××

set -euo pipefail

BMC_HOST="××.××.××.××"
BMC_USER="×××××"
BMC_PASS='×××××'

# 本地采集间隔,默认 10 秒
INTERVAL="${INTERVAL:-10}"

# 飞书群机器人 Webhook
# 把下面的 xxxxxxxx 替换成你自己的 Webhook 后半段
FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx}"

# 如果飞书机器人开启了"签名校验",把 secret 填到这里
# 如果没开启签名校验,保持为空即可
FEISHU_SECRET="${FEISHU_SECRET:-}"

# 每采集几次发一次飞书
# 1 = 每 10 秒发一条
# 6 = 每 6 条汇总发一次,大约每分钟一次,推荐避免刷屏
FEISHU_SEND_EVERY="${FEISHU_SEND_EVERY:-1}"

# 是否发送启动通知:1 发送,0 不发送
FEISHU_SEND_START_NOTICE="${FEISHU_SEND_START_NOTICE:-1}"

# 是否打印 IPMI 调试输出:1 开启,0 关闭
DEBUG_IPMI="${DEBUG_IPMI:-0}"

for c in ipmitool timeout awk date sleep python3; do
    command -v "$c" >/dev/null 2>&1 || {
        echo "ERROR: 未找到 $c,请在普通 Linux 上安装相关工具" >&2
        exit 1
    }
done

if [[ "$FEISHU_WEBHOOK" == *"xxxxxxxx"* ]] || [[ ! "$FEISHU_WEBHOOK" =~ ^https:// ]]; then
    echo "ERROR: 请先把 Get_BMC_Feishu.sh 里的 FEISHU_WEBHOOK 改成你的飞书机器人 Webhook 地址。" >&2
    exit 1
fi

if ! [[ "$FEISHU_SEND_EVERY" =~ ^[0-9]+$ ]]; then
    echo "ERROR: FEISHU_SEND_EVERY 必须是正整数,例如 1 或 6" >&2
    exit 1
fi
FEISHU_SEND_EVERY=$((10#$FEISHU_SEND_EVERY))
if (( FEISHU_SEND_EVERY < 1 )); then
    echo "ERROR: FEISHU_SEND_EVERY 必须大于 0" >&2
    exit 1
fi

if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]]; then
    echo "ERROR: INTERVAL 必须是正整数,例如 10" >&2
    exit 1
fi
INTERVAL=$((10#$INTERVAL))
if (( INTERVAL < 1 )); then
    echo "ERROR: INTERVAL 必须大于 0" >&2
    exit 1
fi

# 可选 ipmitool 参数,例如:IPMI_EXTRA="-C 17" ./Get_BMC_Feishu.sh
IPMI_EXTRA_ARGS=()
if [[ -n "${IPMI_EXTRA:-}" ]]; then
    read -r -a IPMI_EXTRA_ARGS <<< "$IPMI_EXTRA"
fi

ipmi() {
    local -a cmd

    # -E 表示从环境变量 IPMI_PASSWORD 读取密码,避免密码出现在命令行参数里
    cmd=(timeout 15 ipmitool -I lanplus -H "$BMC_HOST" -U "$BMC_USER" -E)

    if ((${#IPMI_EXTRA_ARGS[@]})); then
        cmd+=("${IPMI_EXTRA_ARGS[@]}")
    fi

    cmd+=("$@")

    IPMI_PASSWORD="$BMC_PASS" "${cmd[@]}"
}

send_feishu_text() {
    local text="$1"

    python3 - "$FEISHU_WEBHOOK" "$FEISHU_SECRET" "$text" <<'PY'
import sys
import json
import time
import hmac
import hashlib
import base64
import urllib.request
import urllib.error

webhook, secret, text = sys.argv[1], sys.argv[2], sys.argv[3]

payload = {
    "msg_type": "text",
    "content": {
        "text": text
    }
}

# 飞书自定义机器人签名校验
if secret:
    timestamp = str(int(time.time()))
    string_to_sign = "{}\n{}".format(timestamp, secret)

    # 飞书签名算法:
    # 以 timestamp + "\n" + secret 作为 HMAC key,对空消息做 HMAC-SHA256,再 Base64
    hmac_code = hmac.new(
        string_to_sign.encode("utf-8"),
        b"",
        digestmod=hashlib.sha256
    ).digest()

    sign = base64.b64encode(hmac_code).decode("utf-8")
    payload["timestamp"] = timestamp
    payload["sign"] = sign

data = json.dumps(payload, ensure_ascii=False).encode("utf-8")

req = urllib.request.Request(
    webhook,
    data=data,
    headers={"Content-Type": "application/json"},
    method="POST",
)

try:
    with urllib.request.urlopen(req, timeout=10) as resp:
        body = resp.read().decode("utf-8", "replace")
        status = resp.getcode()

        if status < 200 or status >= 300:
            print("Feishu HTTP status:", status, body, file=sys.stderr)
            sys.exit(1)

        try:
            j = json.loads(body)
            if not isinstance(j, dict):
                j = {}
        except Exception:
            j = {}

        if "StatusCode" in j:
            code = j.get("StatusCode")
        elif "code" in j:
            code = j.get("code")
        else:
            code = 0

        msg = j.get("StatusMessage", j.get("msg", ""))

        if code not in (0, "0", None):
            print("Feishu API error:", code, msg, body, file=sys.stderr)
            sys.exit(1)

except urllib.error.HTTPError as e:
    print("Feishu HTTPError:", e.code, e.read().decode("utf-8", "replace"), file=sys.stderr)
    sys.exit(1)
except Exception as e:
    print("Feishu send failed:", repr(e), file=sys.stderr)
    sys.exit(1)
PY
}

get_dcmi_power() {
    local out power

    if ! out="$(ipmi dcmi power reading 2>&1)"; then
        if [[ "$DEBUG_IPMI" == "1" ]]; then
            echo "DEBUG: dcmi power reading 失败:$out" >&2
        fi
        return 1
    fi

    power="$(awk -F: '/Instantaneous power reading/ {
        v=$2
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
        print v
        exit
    }' <<< "$out")"

    if [[ -z "$power" ]]; then
        if [[ "$DEBUG_IPMI" == "1" ]]; then
            echo "DEBUG: dcmi 输出未解析到功耗:$out" >&2
        fi
        return 1
    fi

    printf '%s\n' "$power"
}

get_sensor_power() {
    local out power

    if ! out="$(ipmi sensor 2>&1)"; then
        if [[ "$DEBUG_IPMI" == "1" ]]; then
            echo "DEBUG: ipmitool sensor 失败:$out" >&2
        fi
        return 1
    fi

    power="$(awk -F'|' '
        BEGIN { best=""; found=0 }
        function trim(s) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", s); return s }
        function number(s) {
            if (match(s, /[0-9]+([.][0-9]+)?/)) return substr(s, RSTART, RLENGTH)
            return ""
        }
        {
            name=trim($1); val=trim($2); unit=trim($3)
            n=number(val)
            low=tolower($0)
            u=tolower(unit)
            ln=tolower(name)

            is_watt=(u ~ /^(w|watt|watts)$/ || low ~ /watt/ || low ~ /[[:space:]]w[[:space:]]/)

            if (n != "" && is_watt) {
                unit_out = (u ~ /^(w|watt|watts)$/ ? unit : "W")
                line=n " " unit_out

                # 优先选择整机/总功耗类传感器
                if (ln ~ /system|total|power meter|server|pwr consumption|power consumption/) {
                    print line
                    found=1
                    exit
                }

                if (best == "") best=line
            }
        }
        END { if (!found && best != "") print best }
    ' <<< "$out")"

    if [[ -z "$power" ]]; then
        if [[ "$DEBUG_IPMI" == "1" ]]; then
            echo "DEBUG: sensor 输出未解析到功耗:$out" >&2
        fi
        return 1
    fi

    printf '%s (sensor fallback)\n' "$power"
}

read_power_once() {
    local power

    if power="$(get_dcmi_power)"; then
        printf '%s\n' "$power"
        return 0
    fi

    if power="$(get_sensor_power)"; then
        printf '%s\n' "$power"
        return 0
    fi

    return 1
}

declare -a FEISHU_BUFFER=()

flush_feishu_buffer() {
    if (( ${#FEISHU_BUFFER[@]} > 0 )); then
        local msg
        msg="$(printf '%s\n' "${FEISHU_BUFFER[@]}")"

        if send_feishu_text "$msg"; then
            :
        else
            echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARN: 发送飞书失败" >&2
        fi

        FEISHU_BUFFER=()
    fi
}

queue_feishu() {
    local line="$1"

    FEISHU_BUFFER+=("$line")

    if (( ${#FEISHU_BUFFER[@]} >= FEISHU_SEND_EVERY )); then
        flush_feishu_buffer
    fi
}

cleanup() {
    flush_feishu_buffer >/dev/null 2>&1 || true
    echo
    echo "已停止 BMC 功耗采集和飞书上报。"
    exit 0
}

trap cleanup INT TERM

if [[ "$FEISHU_SEND_START_NOTICE" == "1" ]]; then
    notice="BMC 功耗采集已启动:目标 ${BMC_HOST},本地采集间隔 ${INTERVAL} 秒,飞书每 ${FEISHU_SEND_EVERY} 条采集上报一次。"
    if ! send_feishu_text "$notice"; then
        echo "WARN: 飞书启动通知发送失败,请检查 Webhook、关键词、IP 白名单或签名 Secret。" >&2
    fi
fi

echo "开始每 ${INTERVAL} 秒读取一次 BMC(${BMC_HOST}) 功耗,并输出到飞书,按 Ctrl+C 停止。"

while true; do
    cycle_start="$(date +%s)"
    ts="$(date '+%Y-%m-%d %H:%M:%S')"

    if power="$(read_power_once)"; then
        line="[$ts] BMC功耗($BMC_HOST) power: $power"
        printf '%s\n' "$line"
    else
        line="[$ts] ERROR: BMC功耗($BMC_HOST) 读取失败"
        printf '%s\n' "$line" >&2
    fi

    queue_feishu "$line"

    cycle_end="$(date +%s)"
    elapsed=$((cycle_end - cycle_start))
    sleep_for=$((INTERVAL - elapsed))

    if (( sleep_for > 0 )); then
        sleep "$sleep_for"
    fi
done
EOF

然后修改代码权限,即可完成相关操作:

bash 复制代码
chmod 700 Get_BMC_Feishu.sh

然后开始执行脚本,可以实时查看脚本功率:

4 在飞书机器人上面实现后台运行

想要在后天执行飞书机器人操作,首先先确定脚本位置:

bash 复制代码
whoami

以及

bash 复制代码
echo "$HOME"

然后查看脚本权限:

bash 复制代码
ls -l "$HOME/Get_BMC_Feishu.sh"

查询用户信息:

bash 复制代码
id 用户

查询完毕之后,写如下所示代码:

bash 复制代码
sudo tee /etc/systemd/system/bmc-power-feishu.service >/dev/null <<'EOF'
[Unit]
Description=Monitor BMC power and send to Feishu
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=××
WorkingDirectory=/××/××

# 每采集 1 次发一次飞书,约 10 秒一条
# 如果嫌刷屏,可以改成 6,约每分钟汇总发一次
Environment=FEISHU_SEND_EVERY=1
Environment=FEISHU_SEND_START_NOTICE=1

# 如果之前 ipmitool 需要 -C 17,可以取消下面这一行注释
# Environment=IPMI_EXTRA=-C 17

ExecStart=/bin/bash /home/x×/Get_BMC_Feishu.sh

Restart=always
RestartSec=10
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
EOF

脚本写完之后修改权限:

bash 复制代码
chmod 700 /home/××/Get_BMC_Feishu.sh
sudo chmod 644 /etc/systemd/system/bmc-power-feishu.service

确认服务文件有没有写错:

bash 复制代码
grep -nE 'User=|WorkingDirectory=|ExecStart=|Restart=' /etc/systemd/system/bmc-power-feishu.service

然后执行下面这几条指令:

bash 复制代码
sudo systemctl daemon-reload
sudo systemctl enable bmc-power-feishu.service
sudo systemctl restart bmc-power-feishu.service
sudo systemctl status bmc-power-feishu.service --no-pager

这几条的指令分别是:sudo systemctl daemon-reload,让systemd 重新读一遍磁盘上的 unit 文件,刷新它内存里的配置表;sudo systemctl enable bmc-power-feishu.servic,登记开机自启,本质是在multi-user.target.wants/里建立软链接。sudo systemctl restart bmc-power-feishu.service,按照新配跑起来;sudo systemctl status bmc-power-feishu.service --no-pager验收查看是否成功运行跑起来,正常情况下是active (running)。

现在想要查看最近日志:

bash 复制代码
sudo journalctl -u bmc-power-feishu.service -n 100 --no-pager

如果想要持续查看日志:

bash 复制代码
sudo journalctl -u bmc-power-feishu.service -f
相关推荐
时间售卖铺1 小时前
Chrony与SSH实战:精准时间同步与安全远程登录
运维·安全·云原生·ssh
微订校园外卖系统1 小时前
校园代取快递系统选型:现成系统与定制开发的技术核对项
大数据·运维·人工智能·外卖系统·本地生活·微订
QXWZ_IA1 小时前
管道GIS测绘如何实现自动化成图:三种成图方式怎么选
运维·人工智能·安全·自动化
一条破秋裤1 小时前
【1112】Linux中断【整理】
linux·运维·服务器
分布式存储与RustFS2 小时前
OpenObserve 的日志别堆本地盘:用 RustFS 当它的对象存储底座
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
新*3 小时前
ES 7.17 三节点 Docker 集群接入 IK 自定义词典:一次从丢插件到热更新的完整排障
运维·elasticsearch·docker·中文分词·ik分词
风翼靓崽3 小时前
jenkins搭建与简单使用
运维·jenkins
lzx_00215 小时前
Linux 进程(三) 从进程状态到僵尸进程、孤儿进程
linux·服务器
柳鲲鹏20 小时前
内斗学概论:43、平级内斗
笔记