跳到主要内容
v4-2026.10.12-代码更加完整强大
python脚本 ipmi

v4-2026.10.12-代码更加完整强大

3829 字 18 分钟 -- 阅读
最后更新于:
AI 摘要

批量带外 IPMI 电源状态检查

以下脚本来源 @咸鱼cursor,创建者@One,记录于2026年10月12日22:07:42。

基于 ipmitool,从 Excel 读取多台服务器带外信息,批量执行 IPMI 命令,结果输出到控制台并保存为 Excel。

初始需求

 1请帮我写一个python脚本(基于 ipmi_status.py 这个脚本内容),用于批量在数台物理服务器带外执行ipmi命令。
 2
 3要求:
 41.要执行的ip可以从 ip.xlsx 文件 里的 Sheet1 的A2~An单元格里获取(A列单元格名称叫 带外ip),带外用户名、带外密码 等分别从 Sheet1 的B2~Bn、C2~Cn单元格 里获取;
 52.要求
 6	将输出结果保存到  result-`date +%F_%T`.xlsx 文件。
 7	a.将ip.xlsx 文件 里的 Sheet1 的A列内容拷贝到新创建的这个 result-`date +%F_%T`.xlsx 文件里的A列(名称叫做 带外ip);
 8	b.将每个ip的执行结果写到 result-`date +%F_%T`.xlsx文件里的每一行 B2~Bn 单元格里(需要新增一列 B列,名称叫做 执行结果摘要),这里只能是 成功或者失败 两个字段;
 9	c.将每个ip的执行结果写到 result-`date +%F_%T`.xlsx文件里的每一行 C2~Cn 单元格里(需要新增一列 C列,名称叫做 执行结果详情);
10    
113.如果遇到某个带外ip无法无法ping通、用户名或口令错误等问题时,可以跳过这个ip(注意,检查时长不要太长),并用红色标注整行单元格;
124.如果要执行的linux命令有回显,就把实际执行结果写到 C2~Cn 单元格里;如果没有回显,若执行成功,则执行结果为"ip 执行成功!!!",若执行失败,则执行结果为"ip 执行失败。。。",同样也要把实际执行结果写到 C2~Cn 单元格里(B2~Bn也要注入 成功或者失败字段);
13
14
15请输出,请务必保证没bug,多谢。

脚本位置

D:\BaiduSyncdisk\python-desktop\02.Python脚本\01-ipmi脚本\查看机器电源状态\v4-2026.10.12-输出控制台同时保存到csv-代码更加完整-cursor实现-亲测有效-工作在用

线上下载:

https://oneimg.onedayxyy.cn/images/2026.10.12-ipmi_status-scripts.zip

脚本内容

  • ipmi_status.py内容
  1# -*- coding: utf-8 -*-
  2"""
  3批量带外执行 IPMI 命令:
  4- 从 ip.xlsx (Sheet1) 读取 带外ip / 用户名 / 密码
  5- 结果写入 result-YYYY-MM-DD_HH-MM-SS.xlsx
  6- ping 不通或认证失败时跳过,整行标红
  7"""
  8
  9from __future__ import annotations
 10
 11import numbers
 12import os
 13import platform
 14import re
 15import shutil
 16import subprocess
 17import sys
 18import time
 19from datetime import datetime
 20from pathlib import Path
 21
 22try:
 23    from openpyxl import Workbook, load_workbook
 24    from openpyxl.styles import Alignment, Font, PatternFill
 25except ImportError:
 26    print("缺少依赖 openpyxl,请先执行: pip install openpyxl")
 27    sys.exit(1)
 28
 29# ======================== 配置区 ========================
 30IP_XLSX = Path(__file__).resolve().parent / "ip.xlsx"
 31SHEET_NAME = "Sheet1"
 32
 33# 可修改为其它 ipmitool 子命令,例如: "chassis status" / "fru print" / "sdr elist"
 34IPMI_SUBCOMMAND = "power status"
 35
 36# 超时(秒)——保持较短,避免卡住
 37PING_TIMEOUT_SEC = 1
 38IPMI_TIMEOUT_SEC = 8
 39# ipmitool 局域网重试:-N 间隔秒, -R 重试次数(尽量短)
 40IPMI_RETRY_INTERVAL = 1
 41IPMI_RETRY_COUNT = 1
 42
 43# 主机间间隔,减轻 BMC 压力
 44HOST_SLEEP_SEC = 0.3
 45
 46# 结果文件被占用时,最多再尝试换名保存的次数
 47SAVE_RETRY_MAX = 20
 48
 49RED_FILL = PatternFill(start_color="FF6B6B", end_color="FF6B6B", fill_type="solid")
 50HEADER_FONT = Font(bold=True)
 51# =======================================================
 52
 53
 54def result_filename(suffix: str = "") -> Path:
 55    """对应 shell: result-$(date +%F_%T).xlsx ;Windows 文件名不能含 ':',用 '-' 代替。"""
 56    stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
 57    name = f"result-{stamp}{suffix}.xlsx"
 58    return Path(__file__).resolve().parent / name
 59
 60
 61def ping_ok(ip: str) -> bool:
 62    """快速探测主机是否可达。"""
 63    system = platform.system().lower()
 64    if system == "windows":
 65        # -n 次数, -w 超时毫秒
 66        cmd = ["ping", "-n", "1", "-w", str(int(PING_TIMEOUT_SEC * 1000)), ip]
 67    else:
 68        # -c 次数, -W 超时秒
 69        cmd = ["ping", "-c", "1", "-W", str(max(1, int(PING_TIMEOUT_SEC))), ip]
 70
 71    try:
 72        proc = subprocess.run(
 73            cmd,
 74            stdout=subprocess.DEVNULL,
 75            stderr=subprocess.DEVNULL,
 76            timeout=PING_TIMEOUT_SEC + 2,
 77        )
 78        return proc.returncode == 0
 79    except (subprocess.TimeoutExpired, OSError):
 80        return False
 81
 82
 83def is_auth_or_session_error(text: str) -> bool:
 84    """根据 ipmitool 回显判断是否为认证/会话类错误(避免裸匹配 session)。"""
 85    if not text:
 86        return False
 87    patterns = [
 88        r"unable to establish\s+(ipmi|lan|rmcp)",
 89        r"unable to establish.*session",
 90        r"rakp\s*[0-9]?",
 91        r"rakp.*hmac",
 92        r"authentication\s+(failure|failed|error|invalid)",
 93        r"invalid\s+password",
 94        r"password\s+invalid",
 95        r"unauthorized",
 96        r"access\s+denied",
 97        r"insufficient\s+privilege",
 98        r"privilege\s+level",
 99        r"command response could not be provided",
100        r"error:\s*unable to",
101    ]
102    lower = text.lower()
103    return any(re.search(p, lower) for p in patterns)
104
105
106def build_ipmitool_cmd(ip: str, user: str) -> list[str]:
107    """
108    解析 ipmitool 可执行文件。
109    Windows 上 .cmd/.bat 需经 cmd /c 启动,否则 subprocess 会 FileNotFoundError。
110    """
111    exe = shutil.which("ipmitool")
112    if not exe:
113        raise FileNotFoundError("ipmitool not found in PATH")
114
115    args = [
116        "-I", "lanplus",
117        "-H", ip,
118        "-U", user,
119        "-E",
120        "-N", str(IPMI_RETRY_INTERVAL),
121        "-R", str(IPMI_RETRY_COUNT),
122        *IPMI_SUBCOMMAND.split(),
123    ]
124    if platform.system().lower() == "windows" and exe.lower().endswith((".cmd", ".bat")):
125        return ["cmd", "/c", exe, *args]
126    return [exe, *args]
127
128
129def run_ipmi(ip: str, user: str, password: str) -> tuple[bool, str, bool]:
130    """
131    执行 IPMI 命令。
132    使用 -E + 环境变量传密,避免密码出现在进程命令行中。
133    返回: (是否成功, 详情文本, 是否应整行标红跳过)
134    """
135    env = os.environ.copy()
136    env["IPMI_PASSWORD"] = password
137
138    try:
139        cmd = build_ipmitool_cmd(ip, user)
140        proc = subprocess.run(
141            cmd,
142            capture_output=True,
143            text=True,
144            timeout=IPMI_TIMEOUT_SEC,
145            encoding="utf-8",
146            errors="replace",
147            env=env,
148        )
149    except subprocess.TimeoutExpired:
150        return False, f"{ip} IPMI 执行超时(>{IPMI_TIMEOUT_SEC}s)", True
151    except FileNotFoundError:
152        return False, "未找到 ipmitool,请确认已安装并加入 PATH", True
153    except OSError as exc:
154        return False, f"执行 ipmitool 失败: {exc}", True
155
156    stdout = (proc.stdout or "").strip()
157    stderr = (proc.stderr or "").strip()
158    combined = "\n".join(x for x in (stdout, stderr) if x).strip()
159    success = proc.returncode == 0
160
161    # 认证/会话错误:视为跳过并标红
162    if (not success) and is_auth_or_session_error(combined):
163        detail = combined if combined else f"{ip} 用户名或口令错误 / 无法建立 IPMI 会话"
164        return False, detail, True
165
166    if combined:
167        detail = combined
168    else:
169        detail = f"{ip} 执行成功!!!" if success else f"{ip} 执行失败。。。"
170
171    # 无回显的失败也标红,便于排查连通性问题
172    mark_red = (not success) and not combined
173    return success, detail, mark_red
174
175
176def cell_str(value) -> str:
177    """
178    单元格转字符串。
179    Excel 常把纯数字密码存成 float(如 123456.0),需还原为 "123456"。
180    """
181    if value is None:
182        return ""
183    if isinstance(value, bool):
184        return str(value).strip()
185    if isinstance(value, numbers.Integral):
186        return str(int(value))
187    if isinstance(value, numbers.Real):
188        num = float(value)
189        if num.is_integer():
190            return str(int(num))
191        text = format(num, "f").rstrip("0").rstrip(".")
192        return text
193    return str(value).strip()
194
195
196def load_targets(xlsx_path: Path) -> list[dict]:
197    """从 Sheet1 读取 A/B/C 列(第 2 行起)。"""
198    if not xlsx_path.exists():
199        raise FileNotFoundError(f"找不到输入文件: {xlsx_path}")
200
201    wb = load_workbook(xlsx_path, read_only=True, data_only=True)
202    if SHEET_NAME not in wb.sheetnames:
203        wb.close()
204        raise ValueError(f"工作簿中不存在工作表: {SHEET_NAME}")
205
206    ws = wb[SHEET_NAME]
207    rows = []
208    for idx, row in enumerate(ws.iter_rows(min_row=2, max_col=3, values_only=True), start=2):
209        ip = cell_str(row[0] if len(row) > 0 else None)
210        user = cell_str(row[1] if len(row) > 1 else None)
211        password = cell_str(row[2] if len(row) > 2 else None)
212        if not ip:
213            continue
214        rows.append({"row": idx, "ip": ip, "user": user, "password": password})
215    wb.close()
216    return rows
217
218
219def apply_row_red(ws, row_idx: int) -> None:
220    for col in range(1, 4):
221        ws.cell(row=row_idx, column=col).fill = RED_FILL
222
223
224def build_workbook(results: list[dict]) -> Workbook:
225    wb = Workbook()
226    ws = wb.active
227    ws.title = SHEET_NAME
228
229    ws["A1"] = "带外ip"
230    ws["B1"] = "执行结果摘要"
231    ws["C1"] = "执行结果详情"
232    for col in range(1, 4):
233        ws.cell(row=1, column=col).font = HEADER_FONT
234
235    for i, item in enumerate(results, start=2):
236        ws.cell(row=i, column=1, value=item["ip"])
237        ws.cell(row=i, column=2, value=item["summary"])
238        cell_c = ws.cell(row=i, column=3, value=item["detail"])
239        cell_c.alignment = Alignment(wrap_text=True, vertical="top")
240        if item.get("mark_red"):
241            apply_row_red(ws, i)
242
243    ws.column_dimensions["A"].width = 18
244    ws.column_dimensions["B"].width = 14
245    ws.column_dimensions["C"].width = 80
246    return wb
247
248
249def write_results(out_path: Path, results: list[dict]) -> Path:
250    """
251    保存结果。若目标文件被占用(PermissionError),自动换名重试。
252    返回实际写入的路径。
253    """
254    if not results:
255        raise ValueError("没有可写入的结果")
256
257    candidates = [out_path]
258    for i in range(1, SAVE_RETRY_MAX + 1):
259        candidates.append(result_filename(suffix=f"-retry{i}"))
260
261    last_err = None
262    for path in candidates:
263        wb = build_workbook(results)
264        try:
265            wb.save(path)
266            return path
267        except PermissionError as exc:
268            last_err = exc
269            print(f"[WARN] 无法写入 {path.name}(可能被 Excel 打开),尝试其它文件名...")
270        finally:
271            wb.close()
272
273    raise PermissionError(
274        f"结果文件多次保存失败(可能均被占用): {out_path.name};最后错误: {last_err}"
275    )
276
277
278def main() -> None:
279    print(f"读取目标列表: {IP_XLSX}")
280    try:
281        targets = load_targets(IP_XLSX)
282    except (FileNotFoundError, ValueError) as exc:
283        print(f"[ERROR] {exc}")
284        sys.exit(1)
285
286    if not targets:
287        print("[ERROR] Sheet1 中未读取到有效的带外 IP(请检查 A2~An)")
288        sys.exit(1)
289
290    out_path = result_filename()
291    results = []
292    total = len(targets)
293    saved_path = None
294
295    print(f"共 {total} 台,IPMI 子命令: {IPMI_SUBCOMMAND}")
296    print(f"结果将写入: {out_path.name}(每台完成后增量保存)")
297    print("-" * 60)
298
299    try:
300        for n, item in enumerate(targets, start=1):
301            ip = item["ip"]
302            user = item["user"]
303            password = item["password"]
304            print(f"[{n}/{total}] {ip} ...", flush=True)
305
306            try:
307                if not user or not password:
308                    detail = f"{ip} 缺少带外用户名或密码,已跳过"
309                    print(f"  -> 失败: {detail}")
310                    results.append(
311                        {"ip": ip, "summary": "失败", "detail": detail, "mark_red": True}
312                    )
313                elif not ping_ok(ip):
314                    detail = f"{ip} 无法 ping 通,已跳过"
315                    print(f"  -> 失败: {detail}")
316                    results.append(
317                        {"ip": ip, "summary": "失败", "detail": detail, "mark_red": True}
318                    )
319                else:
320                    success, detail, mark_red = run_ipmi(ip, user, password)
321                    summary = "成功" if success else "失败"
322                    print(
323                        f"  -> {summary}: {detail[:200]}{'...' if len(detail) > 200 else ''}"
324                    )
325                    results.append(
326                        {
327                            "ip": ip,
328                            "summary": summary,
329                            "detail": detail,
330                            "mark_red": mark_red,
331                        }
332                    )
333            except Exception as exc:  # 单台异常不影响后续主机
334                detail = f"{ip} 处理异常: {exc}"
335                print(f"  -> 失败: {detail}")
336                results.append(
337                    {"ip": ip, "summary": "失败", "detail": detail, "mark_red": True}
338                )
339
340            # 每台完成后立刻落盘,避免中途崩溃丢结果
341            try:
342                saved_path = write_results(out_path, results)
343                out_path = saved_path
344            except PermissionError as exc:
345                print(f"[WARN] 增量保存失败: {exc}")
346
347            if n < total and HOST_SLEEP_SEC > 0:
348                time.sleep(HOST_SLEEP_SEC)
349    finally:
350        # 无论正常结束还是意外中断,尽可能再保存一次
351        if results:
352            try:
353                saved_path = write_results(out_path, results)
354            except Exception as exc:
355                print(f"[ERROR] 最终保存失败: {exc}")
356                if saved_path is None:
357                    sys.exit(1)
358
359    ok = sum(1 for r in results if r["summary"] == "成功")
360    fail = len(results) - ok
361    print("-" * 60)
362    print(f"[DONE] 成功 {ok} / 失败 {fail} / 合计 {len(results)}")
363    print(f"[DONE] 结果已保存: {saved_path or out_path}")
364
365
366if __name__ == "__main__":
367    main()
  • ip.xlsx内容

环境准备

  1. Python:建议 3.8+
  2. 依赖:
1pip install openpyxl
  1. ipmitool:需已安装,且可在命令行直接执行(已加入 PATH)
    • Windows:自行安装 ipmitool 并配置 PATH
    • Linux:如 yum install ipmitool / apt install ipmitool

依赖说明(哪些需要单独安装)

这个脚本需要单独准备的只有两样:

  1. Python 包:openpyxl —— 读写 Excel(ip.xlsx / result-*.xlsx)
  2. 系统命令:ipmitool —— 不是 pip 包,需系统安装并保证能直接执行:

其余都是 Python 标准库(subprocess、pathlib、datetime 等),不用额外安装。

| 类型 | 名称 | 是否要装 |
| --------- | ------------------------------------------------------------ | --------------------- |
| Python 包 | openpyxl | 要(pip install) |
| 系统工具 | ipmitool | 要(系统安装 + PATH) |
| 标准库 | platform / re / subprocess / sys / time / datetime / pathlib | 不用 |

输入文件

ip.xlsx

与脚本同目录,工作表名必须为 Sheet1,从第 2 行开始填写:

列表头(第 1 行)内容
A带外ipBMC / 带外管理 IP
B带外用户名IPMI 用户名
C带外密码IPMI 密码

示例:

带外ip带外用户名带外密码
192.168.1.10adminyour_password
192.168.1.11adminyour_password

空 IP 行会自动跳过;缺用户名或密码的行记为失败并整行标红。

运行

在脚本所在目录执行:

1python ipmi_status.py

运行时控制台会打印每台机器的进度与结果摘要。

输出文件

result-日期_时间.xlsx

文件名示例:result-2026-10-12_21-54-15.xlsx
(对应 Linux 的 date +%F_%T;Windows 文件名不能含 :,时间分隔符用 -)

列表头说明
A带外ip从 ip.xlsx A 列拷贝
B执行结果摘要仅 成功 或 失败
C执行结果详情命令回显,或成功/失败提示语

🟢结果写入规则

  • 有回显:C 列写入实际输出(含 stderr)
  • 无回显且成功:C 列写入 {ip} 执行成功!!!
  • 无回显且失败:C 列写入 {ip} 执行失败。。。
  • ping 不通 / 缺账号密码 / 认证或会话失败 / 超时:记为失败,整行标红,并跳过(不长时间卡住)

常用配置(ipmi_status.py 顶部)

配置项默认值说明
IPMI_SUBCOMMANDpower status要执行的 ipmitool 子命令,可改为 chassis status、fru print 等
PING_TIMEOUT_SEC1ping 超时(秒)
IPMI_TIMEOUT_SEC8单次 IPMI 命令超时(秒)
IPMI_RETRY_INTERVAL1ipmitool -N 重传间隔
IPMI_RETRY_COUNT1ipmitool -R 重试次数
HOST_SLEEP_SEC0.3主机之间的间隔,减轻 BMC 压力

默认实际执行的命令形如(密码通过环境变量 IPMI_PASSWORD + -E 传入,不出现在进程命令行):

1ipmitool -I lanplus -H <ip> -U <user> -E -N 1 -R 1 power status

`-N` / `-R` 参数说明

这是 ipmitool 局域网(lan/lanplus)重传参数,用来控制连不上时别死等太久:

| 参数 | 含义 | 这里的值 |
| ------ | ------------------------ | ----------- |
| -N 1 | 两次重传之间的间隔(秒) | 隔 1 秒再试 |
| -R 1 | 最多重试几次 | 只重试 1 次 |

默认往往是间隔约 1 秒、重试约 4 次,一台 BMC 不通时可能卡很久。脚本里改成 -N 1 -R 1,是为了 ping/认证失败时尽快跳过,批量扫多台时更省时间。

和脚本里的配置对应关系:

  • IPMI_RETRY_INTERVAL → -N
  • IPMI_RETRY_COUNT → -R

脚本说明

这个脚本里哪一块主要是体现要执行的ipmi命令呢?

主要看这三处:

1. 配置:要执行哪条 IPMI 子命令

1# 可修改为其它 ipmitool 子命令,例如: "chassis status" / "fru print" / "sdr elist"
2IPMI_SUBCOMMAND = "power status"

改这里就能换成别的命令,比如 chassis status、fru print。

2. 拼命令:真正的 ipmitool 参数

 1def build_ipmitool_cmd(ip: str, user: str) -> list[str]:
 2    ...
 3    args = [
 4        "-I", "lanplus",
 5        "-H", ip,
 6        "-U", user,
 7        "-E",
 8        "-N", str(IPMI_RETRY_INTERVAL),
 9        "-R", str(IPMI_RETRY_COUNT),
10        *IPMI_SUBCOMMAND.split(),
11    ]

等价于大致:

ipmitool -I lanplus -H <IP> -U <用户> -E -N 1 -R 1 power status

3. 真正执行:run_ipmi()

1def run_ipmi(ip: str, user: str, password: str) -> tuple[bool, str, bool]:
2    ...
3    env["IPMI_PASSWORD"] = password
4    ...
5    cmd = build_ipmitool_cmd(ip, user)
6    proc = subprocess.run(...)

密码走环境变量 IPMI_PASSWORD(配合 -E),用 subprocess.run 跑起来。


主流程里调用点在 main() 的 run_ipmi(ip, user, password)(约第 320 行)。核心就是:IPMI_SUBCOMMAND → build_ipmitool_cmd → run_ipmi。

注意事项

  1. 请确保本机能访问目标带外网段,且防火墙放行 ICMP(ping)与 IPMI(UDP 623)。
  2. ip.xlsx 含明文密码,注意权限与保管,勿随意外传。
  3. 每次运行都会生成新的 result-*.xlsx;若文件正被 Excel 打开导致无法写入,会自动换名重试。
  4. 每台机器处理完会立刻保存一次,中途异常也不易丢掉已查结果。
  5. Excel 里纯数字密码会被正确识别(不会变成 123456.0)。

脚本完善与修复记录

通读后主流程(读 Excel → ping → ipmitool → 写结果/标红)可用。曾发现并处理的问题如下。

曾存在的问题

项问题说明
1Excel 数字密码123456 被存成 float 时会变成 "123456.0",认证失败
2结果文件被占用Excel 打开结果文件时 save 报 PermissionError
3中途异常丢结果未及时落盘时,已查完的结果可能丢失
4session 匹配过宽裸匹配可能误判为认证失败
5强制 ping部分 BMC 关 ICMP,IPMI 可用却被跳过(按原需求保留)
6-P 明文在进程列表密码出现在任务管理器 / ps
7同秒撞名文件名精确到秒,极端情况下可能覆盖

已修复(1 / 2 / 3 / 4 / 6)

项改动
1cell_str 正确处理:123456.0 → 123456
2保存遇 PermissionError 时自动换名重试
3每台处理完立刻写盘,finally 再兜底保存一次
4去掉裸匹配 session,改为更具体的认证/会话失败短语
6改用 -E + IPMI_PASSWORD,密码不再出现在命令行

未改项:5(按需求保留强制 ping)、7(可选,影响极小)。

亲测效果

结束。

最新文章

文档导航