diff --git a/skills/infrastructure/bmc-analyze/README.md b/skills/infrastructure/bmc-analyze/README.md new file mode 100644 index 0000000..e48a7df --- /dev/null +++ b/skills/infrastructure/bmc-analyze/README.md @@ -0,0 +1,178 @@ +# OpenUBMC Forum 故障定位工具 + +本工具集用于 OpenUBMC/iBMC 日志包的自动化故障诊断,提供 Claude Code Skill 和独立 Python 脚本两种使用方式。 + +## 文件说明 + +| 文件 | 用途 | +|------|------| +| `bmc_log_analyzer.py` | 独立 Python 解析脚本,支持 `.tar.gz` 和目录 | +| `bmc_log_mcp_server.py` | MCP Server(6 个工具,stdio 模式) | +| `my_skill/bmc-analyze.md` | Claude Code Skill 定义文件 | + +--- + +## bmc-analyze 使用说明 + +### 概述 + +`/bmc-analyze` 是一个 Claude Code Skill,用于分析 OpenUBMC/iBMC 的 `dump_info` 日志包,自动生成结构化故障诊断报告。 + +### 安装 + +将 `my_skill/bmc-analyze.md` 复制到 Claude Code 的 skills 目录: + +```bash +cp my_skill/bmc-analyze.md ~/.claude/skills/bmc-analyze.md +``` + +### 用法 + +``` +/bmc-analyze [日志包路径] +``` + +**支持的输入格式:** +- `.tar.gz` 压缩包:`/bmc-analyze dump_info.tar.gz` +- 解压后目录:`/bmc-analyze /path/to/dump_info/` +- 不指定路径:自动搜索当前目录下的 `*.tar.gz`、`dump_info*`、`null_*` + +### 报告结构 + +Skill 执行后输出以下诊断报告: + +1. **基本信息** — BMC 类型(iBMC/openUBMC)、固件版本、产品型号 +2. **网络配置** — IP 地址、网口状态 +3. **串口/Telnet 状态**(⚠️ 优先检查)— telnetd 进程是否运行、systemd 服务是否激活 +4. **错误日志摘要** — ERROR 总数、Top 模块分布 +5. **命令历史** — 管理员操作记录 +6. **诊断建议** — 针对发现问题的可操作步骤 + +### 日志文件速查表 + +| 查找内容 | 文件路径 | +|---------|---------| +| BMC 版本 | `RTOSDump/versioninfo/app_revision.txt` | +| IP 地址 | `RTOSDump/networkinfo/ifconfig_info` | +| 串口/Telnet 配置 | `AppDump/bmc_network/network_info.txt` | +| 应用错误日志 | `LogDump/app.log` | +| 操作审计 | `LogDump/operation.log` | +| 内核错误 | `RTOSDump/driver_info/dmesg_info` | +| 服务状态 | `RTOSDump/sysinfo/journalctl.log` | +| 进程列表 | `RTOSDump/sysinfo/ps_info` | +| 管理员历史 | `RTOSDump/other_info/command_records/*/ash_history` | + +### 串口故障专项排查(论坛 #4257 类问题) + +用户反映"无法通过 Telnet 连接 BMC 串口"时,按以下顺序检查: + +1. `RTOSDump/sysinfo/ps_info` — telnetd 进程是否在列 +2. `RTOSDump/sysinfo/journalctl.log*` — telnetd.service 是否被 deactivated +3. `LogDump/operation.log` — 查找 "SOL session" / "Connect SOL failed" +4. `AppDump/bmc_network/network_info.txt` — 端口 23 防火墙规则 + +若 telnetd 服务已停止:通过 iBMC/openUBMC Web 界面 → 系统管理 → 串口配置,重新启用串口转发。 + +--- + +## 示例:分析 dump_info 日志包 + +### 执行命令 + +``` +/bmc-analyze dump_info +``` + +### 输出报告示例 + +以下为对 `dump_info/` 目录(iBMC S920X20 设备)的实际分析结果: + +--- + +#### 一、基本信息 + +| 项目 | 值 | +|------|-----| +| BMC 类型 | iBMC | +| 活跃版本 | 25.12.02.03 (Build 001) | +| 编译时间 | 2026-03-09 13:48:26 | +| 产品型号 | S920X20 | +| BIOS 版本 | (U75)000 | + +#### 二、网络配置 + +| 接口 | IP 地址 | 状态 | +|------|---------|------| +| eth0 | 10.0.2.15 | UP | +| lo | 127.0.0.1 | UP | + +#### 三、串口/Telnet 状态 + +| 项目 | 状态 | +|------|------| +| telnetd 进程 | 运行中 | +| 进程详情 | `/data/home/busybox_x telnetd` (PID 2150) | + +**结论:** telnetd 正常运行,可通过 telnet 登录 BMC。 + +#### 四、错误日志摘要 + +| 日志类型 | 数量 | +|---------|------| +| app.log ERROR | 136 条 | +| 告警日志 | 1061 条 | +| dmesg 异常 | 0 条 | +| maintenance ERROR | 10 条 | + +**Top 模块错误分布:** + +| 模块 | 错误数 | +|------|--------| +| snmp | 43 | +| fault_diagnosis | 22 | +| interface | 18 | +| firmware_mgmt | 12 | +| power_mgmt | 11 | +| event | 11 | +| pcie_device | 9 | + +#### 五、命令历史 + +共 11 条记录,发现 telnet 尝试: + +``` +telnet localhost +``` + +#### 六、诊断建议 + +**主要问题:** app.log 存在 136 条 ERROR,snmp 模块最多(43 条) + +排查步骤: +1. 检查 `LogDump/app.log`,重点关注 `snmp` 模块错误 +2. 查找最早 ERROR 时间戳,定位问题发生时间点 +3. 如需深入排查 snmp:检查 `RTOSDump/sysinfo/journalctl.log` 中 snmp 服务状态 + +**串口/Telnet 方面无异常**,telnetd 进程正常运行。 + +--- + +## 底层脚本直接使用 + +不依赖 Claude Code,也可直接运行 Python 脚本: + +```bash +python3 bmc_log_analyzer.py <日志包路径> +``` + +**示例:** + +```bash +# 分析目录 +python3 bmc_log_analyzer.py ./dump_info + +# 分析压缩包 +python3 bmc_log_analyzer.py ./dump_info.tar.gz +``` + +脚本自动处理 `dump_info/dump_info/`(双层嵌套)和 `null_xxx/dump_info/`(单层嵌套)两种目录结构。 diff --git a/skills/infrastructure/bmc-analyze/SKILL.md b/skills/infrastructure/bmc-analyze/SKILL.md new file mode 100644 index 0000000..0015ddc --- /dev/null +++ b/skills/infrastructure/bmc-analyze/SKILL.md @@ -0,0 +1,79 @@ +# bmc-analyze — OpenUBMC/iBMC 日志包故障诊断 + +分析 OpenUBMC/iBMC 的 dump_info 日志包,生成结构化故障诊断报告。用法:`/bmc-analyze [日志包路径]`,支持 .tar.gz 文件或目录。 + +# 示例: +# /bmc-analyze dump_info.tar.gz +# /bmc-analyze /path/to/dump_info/ + +Analyze the BMC log package at the path provided by the user (or use the current directory if no path given). + +Follow these steps: + +## Step 1: Find the log package + +If the user provided a path, use it directly. Otherwise, look for: +- Files matching `*.tar.gz` in the current directory +- Directories named `dump_info*` or `null_*` in the current directory + +## Step 2: Run the analyzer script + +Use the Bash tool to run: +```bash +python3 /home/zhongjun/claude/zhongjun2/openUBMC_forum/bmc_log_analyzer.py 2>&1 +``` + +If no analyzer script is available, perform the analysis manually: + +### Manual Analysis Checklist + +**A. Version Info** - Read `RTOSDump/versioninfo/app_revision.txt` +- Look for: `Active iBMC Version:` or `Active openUBMC Version:` + +**B. Network Config** - Read `RTOSDump/networkinfo/ifconfig_info` +- Look for: IP addresses, UP/DOWN interface states + +**C. Serial/Telnet Config** - Read these files: +- `RTOSDump/sysinfo/ps_info` or `top_info` → grep for "telnetd" +- `RTOSDump/sysinfo/journalctl.log*` → grep for "telnetd" +- `LogDump/operation.log` → grep for "serial" or "SOL" +- `AppDump/bmc_network/network_info.txt` → check port 23 rules + +**D. Error Logs** - Read `LogDump/app.log` +- Count ERROR lines, identify top modules + +**E. Command History** - Read `RTOSDump/other_info/command_records/*/ash_history` + +## Step 3: Present the report + +Format the findings as a structured report with: +1. **Basic Info**: BMC type, version, product model +2. **Network Status**: IP addresses, interface states +3. **Serial/Telnet Status** (⚠️ PRIORITY): Is telnetd running? Is systemd service active? +4. **Error Summary**: ERROR count, top modules, critical errors +5. **Diagnosis & Recommendations**: Specific actionable steps + +## Key Log File Map (tell the user which file to check for what) + +| What to find | File path | +|---|---| +| BMC version | `RTOSDump/versioninfo/app_revision.txt` | +| IP address | `RTOSDump/networkinfo/ifconfig_info` | +| Serial/telnet config | `AppDump/bmc_network/network_info.txt` | +| App error logs | `LogDump/app.log` | +| Operation audit | `LogDump/operation.log` | +| Kernel errors | `RTOSDump/driver_info/dmesg_info` | +| Service status | `RTOSDump/sysinfo/journalctl.log` | +| Process list | `RTOSDump/sysinfo/ps_info` | +| Admin history | `RTOSDump/other_info/command_records/*/ash_history` | + +## Serial Port Troubleshooting (for forum post #4257 type issues) + +If user reports "cannot connect via telnet to BMC serial port": + +1. Check `RTOSDump/sysinfo/ps_info` - is `telnetd` in process list? +2. Check `RTOSDump/sysinfo/journalctl.log*` - was telnetd.service deactivated? +3. Check `LogDump/operation.log` - look for "SOL session" failures +4. Check `AppDump/bmc_network/network_info.txt` - firewall rules for port 23 + +If telnetd service is stopped: recommend enabling serial forwarding via iBMC/openUBMC web interface → System Management → Serial Port Config. diff --git a/skills/infrastructure/bmc-analyze/bmc_log_analyzer.py b/skills/infrastructure/bmc-analyze/bmc_log_analyzer.py new file mode 100644 index 0000000..0f922bb --- /dev/null +++ b/skills/infrastructure/bmc-analyze/bmc_log_analyzer.py @@ -0,0 +1,714 @@ +#!/usr/bin/env python3 +""" +BMC 日志包分析器 - 独立脚本版本 +用途:解析 OpenUBMC/iBMC 的 dump_info.tar.gz 日志包,生成结构化摘要报告 +版本:v1.0 日期:2026-03-11 +""" + +import os +import re +import sys +import gzip +import tarfile +import argparse +import textwrap +from pathlib import Path +from datetime import datetime + + +# ─── 1. 日志包结构定义 ─────────────────────────────────────────────────────── + +# 关键文件路径模式(相对于 dump_info/ 根目录) +KEY_FILES = { + "version": "RTOSDump/versioninfo/app_revision.txt", + "rtos_release": "RTOSDump/versioninfo/RTOS-Release", + "server_config": "RTOSDump/versioninfo/server_config.txt", + "ifconfig": "RTOSDump/networkinfo/ifconfig_info", + "network_info": "AppDump/bmc_network/network_info.txt", + "netstat": "RTOSDump/networkinfo/netstat_info", + "route": "RTOSDump/networkinfo/route_info", + "sshd_config": "RTOSDump/other_info/sshd_config", + "app_log": "LogDump/app.log", + "operation_log": "LogDump/operation.log", + "alarm_log": "LogDump/alarm.log", + "maintenance_log": "LogDump/maintenance.log", + "dmesg": "RTOSDump/driver_info/dmesg_info", + "journalctl": "RTOSDump/sysinfo/journalctl.log", + "ps_info": "RTOSDump/sysinfo/ps_info", + "top_info": "RTOSDump/sysinfo/top_info", + "cmd_history": "RTOSDump/other_info/command_records/Administrator/ash_history", + "lldp_info": "AppDump/bmc_network/lldp_info.txt", +} + +# ─── 2. 日志包发现与解压 ───────────────────────────────────────────────────── + +def find_dump_root(base_path: Path) -> Path | None: + """在给定路径下找到 dump_info 根目录(处理嵌套结构)""" + # 情况1: base_path 本身就是 dump_info 目录 + if (base_path / "RTOSDump").exists(): + return base_path + if (base_path / "LogDump").exists(): + return base_path + # 情况2: base_path/dump_info/...(单层嵌套) + for sub in base_path.iterdir(): + if sub.is_dir(): + if (sub / "RTOSDump").exists() or (sub / "LogDump").exists(): + return sub + # 情况3: base_path/dump_info/dump_info/...(双层嵌套) + for sub in base_path.iterdir(): + if sub.is_dir(): + for sub2 in sub.iterdir(): + if sub2.is_dir(): + if (sub2 / "RTOSDump").exists() or (sub2 / "LogDump").exists(): + return sub2 + return None + + +def extract_if_needed(path_str: str) -> Path: + """如果是 tar.gz 则解压到临时目录,否则直接返回路径""" + p = Path(path_str) + if not p.exists(): + raise FileNotFoundError(f"路径不存在: {path_str}") + + if p.suffix in (".gz", ".tgz") or path_str.endswith(".tar.gz"): + import tempfile + tmpdir = tempfile.mkdtemp(prefix="bmc_analyze_") + print(f"[INFO] 解压 {p.name} 到 {tmpdir} ...", file=sys.stderr) + with tarfile.open(p, "r:gz") as tar: + tar.extractall(tmpdir) + return Path(tmpdir) + return p + + +# ─── 3. 文件读取工具 ───────────────────────────────────────────────────────── + +def read_file_safe(path: Path, max_bytes: int = 102400) -> str: + """安全读取文件内容,处理编码错误,支持 .gz""" + if not path.exists(): + return "" + try: + if path.suffix == ".gz": + with gzip.open(path, "rt", encoding="utf-8", errors="replace") as f: + return f.read(max_bytes) + else: + with open(path, "r", encoding="utf-8", errors="replace") as f: + return f.read(max_bytes) + except Exception: + return "" + + +def get_file(root: Path, rel_path: str) -> tuple[Path, str]: + """获取文件路径和内容""" + full_path = root / rel_path + content = read_file_safe(full_path) + return full_path, content + + +# ─── 4. 各模块分析函数 ─────────────────────────────────────────────────────── + +def analyze_version(root: Path) -> dict: + """解析版本信息""" + _, content = get_file(root, KEY_FILES["version"]) + result = { + "bmc_version": "未知", + "bmc_type": "iBMC", # iBMC or openUBMC + "bmc_build": "未知", + "bmc_built": "未知", + "backup_version": "未知", + "product_name": "未知", + "bios_version": "未知", + "raw": content[:2000] if content else "", + } + if not content: + return result + + # 检测是iBMC还是openUBMC + if "openUBMC" in content: + result["bmc_type"] = "openUBMC" + m = re.search(r"Active openUBMC\s+Version:\s+(\S+)", content) + else: + result["bmc_type"] = "iBMC" + m = re.search(r"Active iBMC\s+Version:\s+(\S+)", content) + + if m: + result["bmc_version"] = m.group(1) + + m = re.search(r"Active iBMC\s+Build:\s+(\S+)|Active openUBMC\s+Build:\s+(\S+)", content) + if m: + result["bmc_build"] = m.group(1) or m.group(2) or "未知" + + m = re.search(r"Active iBMC\s+Built:\s+(.+)|Active openUBMC\s+Built:\s+(.+)", content) + if m: + result["bmc_built"] = (m.group(1) or m.group(2) or "").strip() + + m = re.search(r"Backup iBMC\s+Version:\s+(\S+)|Backup openUBMC\s+Version:\s+(\S+)", content) + if m: + result["backup_version"] = m.group(1) or m.group(2) or "未知" + + m = re.search(r"Product\s+Name:\s+(.+)", content) + if m: + result["product_name"] = m.group(1).strip() + + m = re.search(r"Active BIOS\s+Version:\s+(.+)", content) + if m: + result["bios_version"] = m.group(1).strip() + + return result + + +def analyze_network(root: Path) -> dict: + """解析网络配置""" + _, ifconfig = get_file(root, KEY_FILES["ifconfig"]) + _, network_info = get_file(root, KEY_FILES["network_info"]) + _, netstat = get_file(root, KEY_FILES["netstat"]) + + result = { + "interfaces": [], + "bmc_ip": "未知", + "active_port": "未知", + "net_mode": "未知", + "telnet_port_open": False, + "telnet_port": 23, + "listening_ports": [], + "firewall_rules_count": 0, + } + + # 解析 ifconfig + if ifconfig: + # 解析各网口 + blocks = re.split(r'\n(?=\w)', ifconfig.strip()) + for block in blocks: + m_iface = re.match(r'^(\w+)\s+', block) + m_ip = re.search(r'inet addr:(\S+)', block) + m_mac = re.search(r'HWaddr (\S+)', block) + m_state = re.search(r'\b(UP|DOWN)\b', block) + if m_iface: + iface = { + "name": m_iface.group(1), + "ip": m_ip.group(1) if m_ip else None, + "mac": m_mac.group(1) if m_mac else None, + "state": m_state.group(1) if m_state else "UNKNOWN", + } + result["interfaces"].append(iface) + if iface["ip"] and iface["ip"] != "127.0.0.1": + result["bmc_ip"] = iface["ip"] + + # 解析 network_info.txt(BMC应用层配置) + if network_info: + m = re.search(r"Active Port\s*:\s*(\S+)", network_info) + if m: + result["active_port"] = m.group(1) + m = re.search(r"Net Mode\s*:\s*(.+)", network_info) + if m: + result["net_mode"] = m.group(1).strip() + # 统计防火墙规则数 + result["firewall_rules_count"] = network_info.count("ACCEPT") + network_info.count("DROP") + + # 解析 netstat(检查监听端口) + if netstat: + for line in netstat.split("\n"): + m = re.search(r"tcp.*:(\d+)\s+.*LISTEN", line) + if m: + port = int(m.group(1)) + result["listening_ports"].append(port) + if port == 23: + result["telnet_port_open"] = True + + return result + + +def analyze_serial_config(root: Path) -> dict: + """专项分析串口/telnet配置(帖子#4257的核心问题)""" + _, ps_info = get_file(root, KEY_FILES["ps_info"]) + _, top_info = get_file(root, KEY_FILES["top_info"]) + # 合并所有 journalctl 文件(.log, .log.1, .log.2, .log.3) + journalctl_all = [] + for jname in ["RTOSDump/sysinfo/journalctl.log", "RTOSDump/sysinfo/journalctl.log.1", + "RTOSDump/sysinfo/journalctl.log.2", "RTOSDump/sysinfo/journalctl.log.3"]: + _, jcontent = get_file(root, jname) + if jcontent: + journalctl_all.append(jcontent) + journalctl = "\n".join(journalctl_all) + _, op_log = get_file(root, KEY_FILES["operation_log"]) + _, netstat = get_file(root, KEY_FILES["netstat"]) + + result = { + "telnetd_running": False, + "telnetd_process": "", + "telnetd_service_state": "未知", # active/inactive/stopped + "serial_operations": [], # 从操作日志提取 + "sol_config": "未知", # SOL (Serial Over LAN) 状态 + "diagnosis": [], # 诊断结论 + } + + # 1. 检查 telnetd 进程 + for source, text in [("ps_info", ps_info), ("top_info", top_info)]: + if text: + for line in text.split("\n"): + if "telnetd" in line.lower() and not line.strip().startswith("#"): + result["telnetd_running"] = True + result["telnetd_process"] = line.strip() + break + + # 2. 从 journalctl 检查 telnetd service 状态 + if journalctl: + # 找最后一条关于 telnetd 的记录 + telnet_events = [] + for line in journalctl.split("\n"): + if "telnetd" in line.lower(): + telnet_events.append(line.strip()) + if telnet_events: + last_event = telnet_events[-1] + if "Deactivated" in last_event or "Stopped" in last_event: + result["telnetd_service_state"] = "已停止(inactive)" + elif "Started" in last_event or "Active" in last_event: + result["telnetd_service_state"] = "已启动(active)" + result["telnetd_last_event"] = last_event + + # 3. 从操作日志提取串口操作 + if op_log: + for line in op_log.split("\n"): + if "serial" in line.lower() or "sol" in line.lower() or "串口" in line: + result["serial_operations"].append(line.strip()) + + # 4. 检查 netstat 中是否有 telnet 端口 23 监听 + if netstat: + telnet_listening = bool(re.search(r":23\s+.*LISTEN", netstat)) + if telnet_listening: + result["telnetd_port_listening"] = True + else: + result["telnetd_port_listening"] = False + + # 5. 生成诊断结论 + if result["telnetd_running"]: + result["diagnosis"].append("✓ telnetd 进程运行中(可通过 telnet 登录 BMC)") + else: + result["diagnosis"].append("✗ 未检测到运行中的 telnetd 进程") + + if result["telnetd_service_state"] == "已停止(inactive)": + result["diagnosis"].append("✗ systemd telnetd.service 已停止,这可能导致无法通过 telnet 登录 BMC 串口") + result["diagnosis"].append("→ 建议:通过 iBMC Web 界面 → 系统管理 → 串口配置,检查并启用串口转发功能") + elif result["telnetd_service_state"] == "已启动(active)": + result["diagnosis"].append("✓ systemd telnetd.service 已启动") + + if result["serial_operations"]: + result["diagnosis"].append(f"ℹ 操作日志中有 {len(result['serial_operations'])} 条串口相关操作记录") + + return result + + +def analyze_errors(root: Path) -> dict: + """分析错误日志""" + result = { + "app_error_count": 0, + "app_errors_top": [], # 按模块统计 + "dmesg_errors": [], + "maintenance_errors": [], + "alarm_count": 0, + "critical_errors": [], # 严重错误(需要关注的) + } + + # app.log 错误分析 + _, app_log = get_file(root, KEY_FILES["app_log"]) + if app_log: + errors = [l for l in app_log.split("\n") if " ERROR:" in l or " FATAL:" in l or " WARNING:" in l] + result["app_error_count"] = len([l for l in errors if " ERROR:" in l or " FATAL:" in l]) + # 按模块统计(日志格式: TIMESTAMP MODULE LEVEL: file(line): msg) + module_counts = {} + for err in errors: + # 格式1: 2023-08-15 09:20:57.640619 devmon ERROR: ... + m = re.match(r"\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\S*\s+(\w+)\s+(?:ERROR|FATAL|WARNING):", err) + if m: + mod = m.group(1) + module_counts[mod] = module_counts.get(mod, 0) + 1 + result["app_errors_top"] = sorted(module_counts.items(), key=lambda x: -x[1])[:10] + # 提取 FATAL 级别 + fatals = [l.strip() for l in app_log.split("\n") if " FATAL:" in l] + result["critical_errors"].extend(fatals[:5]) + + # dmesg 错误 + _, dmesg = get_file(root, KEY_FILES["dmesg"]) + if dmesg: + dmesg_errs = [l.strip() for l in dmesg.split("\n") + if re.search(r'\b(error|fail|panic|oops|BUG|WARN)\b', l, re.I)] + result["dmesg_errors"] = dmesg_errs[:20] + + # maintenance_log 错误 + _, maint = get_file(root, KEY_FILES["maintenance_log"]) + if maint: + maint_errs = [l.strip() for l in maint.split("\n") if " ERROR:" in l] + result["maintenance_errors"] = maint_errs[:10] + + # alarm.log + _, alarm = get_file(root, KEY_FILES["alarm_log"]) + if alarm: + result["alarm_count"] = len([l for l in alarm.split("\n") if l.strip()]) + + return result + + +def analyze_system_state(root: Path) -> dict: + """分析系统状态(内存、进程、运行时间)""" + result = { + "uptime": "未知", + "memory_total": "未知", + "memory_free": "未知", + "top_processes": [], + } + + # uptime + uptime_path = root / "RTOSDump/sysinfo/uptime" + if uptime_path.exists(): + content = read_file_safe(uptime_path) + result["uptime"] = content.strip() + + # memory + free_path = root / "RTOSDump/sysinfo/free_info" + if free_path.exists(): + content = read_file_safe(free_path) + for line in content.split("\n"): + if line.startswith("Mem:"): + parts = line.split() + if len(parts) >= 3: + result["memory_total"] = parts[1] + " kB" + result["memory_free"] = parts[3] + " kB" + + return result + + +def analyze_command_history(root: Path) -> dict: + """分析管理员命令历史""" + # 支持通配符:command_records 下可能有多个用户目录 + cmd_records_dir = root / "RTOSDump/other_info/command_records" + history_content = "" + if cmd_records_dir.exists(): + for user_dir in cmd_records_dir.iterdir(): + if user_dir.is_dir(): + hist_file = user_dir / "ash_history" + if hist_file.exists(): + history_content += read_file_safe(hist_file) + # 兜底:尝试固定路径 + if not history_content: + _, history_content = get_file(root, KEY_FILES["cmd_history"]) + result = { + "has_history": bool(history_content), + "commands": [], + "telnet_attempts": [], + "suspicious_commands": [], + } + if history_content: + cmds = [l.strip() for l in history_content.split("\n") if l.strip()] + result["commands"] = cmds + result["telnet_attempts"] = [c for c in cmds if "telnet" in c.lower()] + # 标记危险操作 + dangerous = ["rm -rf", "chmod 777", "mkfs", "dd if=", "passwd", "> /dev/"] + for cmd in cmds: + for d in dangerous: + if d in cmd: + result["suspicious_commands"].append(cmd) + return result + + +# ─── 5. 报告生成 ───────────────────────────────────────────────────────────── + +def generate_report(root: Path, pkg_name: str) -> str: + """生成完整的 Markdown 格式分析报告""" + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + print("[INFO] 分析版本信息...", file=sys.stderr) + ver = analyze_version(root) + print("[INFO] 分析网络配置...", file=sys.stderr) + net = analyze_network(root) + print("[INFO] 分析串口配置...", file=sys.stderr) + serial = analyze_serial_config(root) + print("[INFO] 分析错误日志...", file=sys.stderr) + errors = analyze_errors(root) + print("[INFO] 分析系统状态...", file=sys.stderr) + sysstate = analyze_system_state(root) + print("[INFO] 分析命令历史...", file=sys.stderr) + cmdhist = analyze_command_history(root) + + lines = [] + + # 标题 + lines.append(f"# BMC 日志分析报告") + lines.append(f"") + lines.append(f"- **日志包**: `{pkg_name}`") + lines.append(f"- **分析时间**: {now}") + lines.append(f"- **日志根目录**: `{root}`") + lines.append(f"") + + # ── 基本信息 ── + lines.append(f"## 一、基本信息") + lines.append(f"") + lines.append(f"| 项目 | 值 |") + lines.append(f"|------|-----|") + lines.append(f"| BMC类型 | {ver['bmc_type']} |") + lines.append(f"| 活跃版本 | **{ver['bmc_version']}** (Build {ver['bmc_build']}) |") + lines.append(f"| 编译时间 | {ver['bmc_built']} |") + lines.append(f"| 备份版本 | {ver['backup_version']} |") + lines.append(f"| 产品型号 | {ver['product_name']} |") + lines.append(f"| BIOS版本 | {ver['bios_version']} |") + lines.append(f"") + + # ── 网络配置 ── + lines.append(f"## 二、网络配置") + lines.append(f"") + lines.append(f"| 项目 | 值 |") + lines.append(f"|------|-----|") + lines.append(f"| BMC IP地址 | **{net['bmc_ip']}** |") + lines.append(f"| 活跃端口 | {net['active_port']} |") + lines.append(f"| 网络模式 | {net['net_mode']} |") + lines.append(f"") + lines.append(f"**网络接口状态:**") + lines.append(f"") + if net["interfaces"]: + lines.append(f"| 接口 | IP地址 | MAC | 状态 |") + lines.append(f"|------|--------|-----|------|") + for iface in net["interfaces"]: + ip = iface.get("ip") or "-" + mac = iface.get("mac") or "-" + state = iface.get("state", "UNKNOWN") + state_icon = "✓" if state == "UP" else "✗" + lines.append(f"| {iface['name']} | {ip} | {mac} | {state_icon} {state} |") + else: + lines.append(f"_未找到网络接口信息_") + lines.append(f"") + + # ── 串口/Telnet 配置(重点!) ── + lines.append(f"## 三、串口/Telnet 配置 ⚠️") + lines.append(f"") + lines.append(f"| 项目 | 值 |") + lines.append(f"|------|-----|") + telnetd_icon = "✓ 运行中" if serial["telnetd_running"] else "✗ 未运行" + lines.append(f"| telnetd 进程 | **{telnetd_icon}** |") + lines.append(f"| telnetd 服务状态 | {serial['telnetd_service_state']} |") + lines.append(f"") + + if serial["telnetd_process"]: + lines.append(f"**进程信息:**") + lines.append(f"```") + lines.append(serial["telnetd_process"][:200]) + lines.append(f"```") + lines.append(f"") + + lines.append(f"**诊断结论:**") + lines.append(f"") + for d in serial["diagnosis"]: + lines.append(f"- {d}") + lines.append(f"") + + if serial["serial_operations"]: + lines.append(f"**操作日志中的串口操作记录(最近 {min(5, len(serial['serial_operations']))} 条):**") + lines.append(f"") + lines.append(f"```") + for op in serial["serial_operations"][-5:]: + lines.append(op[:200]) + lines.append(f"```") + lines.append(f"") + + # ── 错误日志摘要 ── + lines.append(f"## 四、错误日志摘要") + lines.append(f"") + lines.append(f"| 日志类型 | 数量 |") + lines.append(f"|---------|------|") + lines.append(f"| app.log ERROR 条数 | {errors['app_error_count']} |") + lines.append(f"| 告警日志条数 | {errors['alarm_count']} |") + lines.append(f"| dmesg 异常条数 | {len(errors['dmesg_errors'])} |") + lines.append(f"| maintenance ERROR 条数 | {len(errors['maintenance_errors'])} |") + lines.append(f"") + + if errors["app_errors_top"]: + lines.append(f"**app.log 错误按模块分布(Top 10):**") + lines.append(f"") + lines.append(f"| 模块 | 错误数 |") + lines.append(f"|------|--------|") + for mod, cnt in errors["app_errors_top"]: + lines.append(f"| {mod} | {cnt} |") + lines.append(f"") + + if errors["critical_errors"]: + lines.append(f"**FATAL 级别错误:**") + lines.append(f"") + lines.append(f"```") + for e in errors["critical_errors"]: + lines.append(e[:200]) + lines.append(f"```") + lines.append(f"") + + if errors["dmesg_errors"]: + lines.append(f"**dmesg 关键错误(前20条):**") + lines.append(f"") + lines.append(f"```") + for e in errors["dmesg_errors"][:10]: + lines.append(e[:200]) + lines.append(f"```") + lines.append(f"") + + # ── 命令历史 ── + if cmdhist["has_history"]: + lines.append(f"## 五、管理员命令历史") + lines.append(f"") + lines.append(f"共 {len(cmdhist['commands'])} 条命令记录。") + lines.append(f"") + + if cmdhist["telnet_attempts"]: + lines.append(f"**telnet 尝试记录:**") + lines.append(f"```") + for c in cmdhist["telnet_attempts"]: + lines.append(c) + lines.append(f"```") + lines.append(f"") + + if cmdhist["suspicious_commands"]: + lines.append(f"**⚠️ 注意:发现可能的危险操作:**") + lines.append(f"```") + for c in cmdhist["suspicious_commands"]: + lines.append(c) + lines.append(f"```") + lines.append(f"") + + # ── 日志文件地图 ── + lines.append(f"## 六、日志文件导航地图") + lines.append(f"") + lines.append(f"以下是主要日志文件的位置说明,便于手动深入排查:") + lines.append(f"") + lines.append(f"| 查找内容 | 文件路径 | 说明 |") + lines.append(f"|---------|---------|------|") + file_map = [ + ("BMC版本", "RTOSDump/versioninfo/app_revision.txt", "首要检查:版本号、编译时间"), + ("IP地址", "RTOSDump/networkinfo/ifconfig_info", "网口IP、MAC、状态"), + ("BMC网络配置", "AppDump/bmc_network/network_info.txt", "IP模式、VLAN、防火墙规则"), + ("串口配置", "AppDump/bmc_network/network_info.txt", "⚠️ telnet串口配置在此"), + ("错误日志", "LogDump/app.log", "应用层ERROR日志"), + ("操作日志", "LogDump/operation.log", "用户操作审计(含串口操作)"), + ("告警日志", "LogDump/alarm.log", "告警事件记录"), + ("内核日志", "RTOSDump/driver_info/dmesg_info", "驱动错误、内核崩溃"), + ("服务状态", "RTOSDump/sysinfo/journalctl.log", "systemd服务启动/停止"), + ("进程状态", "RTOSDump/sysinfo/ps_info", "当前运行进程"), + ("命令历史", "RTOSDump/other_info/command_records/*/ash_history", "管理员执行的命令"), + ("维护日志", "LogDump/maintenance.log", "周期性维护任务日志"), + ("启动日志", "AppDump/dfm/", "BMC启动过程日志"), + ("存储信息", "LogDump/storage/", "RAID/磁盘状态"), + ] + for item, path, desc in file_map: + full = root / path + exists = "✓" if (root / path.rstrip("/")).exists() else "✗" + lines.append(f"| {item} | `{path}` | {exists} {desc} |") + lines.append(f"") + + # ── 诊断建议 ── + lines.append(f"## 七、综合诊断建议") + lines.append(f"") + + suggestions = [] + + # 串口问题 + if not serial["telnetd_running"] or serial["telnetd_service_state"] == "已停止(inactive)": + suggestions.append({ + "level": "HIGH", + "issue": "BMC 串口/telnet 服务未运行", + "steps": [ + "登录 iBMC/openUBMC Web 界面", + "进入:系统管理 → 串口配置", + '确认"串口转发"或"SOL"功能已启用', + "检查 telnet 端口(默认23)防火墙规则", + "参考文件:AppDump/bmc_network/network_info.txt(查看防火墙规则)", + ] + }) + + # 大量错误 + if errors["app_error_count"] > 100: + top_mod = errors["app_errors_top"][0][0] if errors["app_errors_top"] else "未知" + suggestions.append({ + "level": "MEDIUM", + "issue": f"app.log 中存在大量 ERROR({errors['app_error_count']} 条),主要来自模块:{top_mod}", + "steps": [ + f"检查 LogDump/app.log,重点关注模块 '{top_mod}' 的错误", + "查找最早的 ERROR 时间戳,定位问题发生时间点", + ] + }) + + # dmesg 错误 + if errors["dmesg_errors"]: + suggestions.append({ + "level": "MEDIUM", + "issue": f"dmesg 中有 {len(errors['dmesg_errors'])} 条内核异常", + "steps": [ + "检查 RTOSDump/driver_info/dmesg_info", + "重点关注 'error'、'fail'、'panic' 等关键词", + ] + }) + + if not suggestions: + lines.append(f"未发现明显问题,建议进一步分析 `LogDump/app.log` 中的详细错误信息。") + else: + for i, s in enumerate(suggestions, 1): + level_icon = "🔴" if s["level"] == "HIGH" else "🟡" + lines.append(f"### {level_icon} 问题 {i}:{s['issue']}") + lines.append(f"") + lines.append(f"**排查步骤:**") + for j, step in enumerate(s["steps"], 1): + lines.append(f"{j}. {step}") + lines.append(f"") + + lines.append(f"---") + lines.append(f"_报告由 bmc_log_analyzer.py v1.0 自动生成_") + + return "\n".join(lines) + + +# ─── 6. 主程序 ─────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser( + description="OpenUBMC/iBMC 日志包分析工具", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(""" + 示例: + # 分析解压后的目录 + python3 bmc_log_analyzer.py dump_info/ + + # 分析 tar.gz 包(自动解压) + python3 bmc_log_analyzer.py dump_info.tar.gz + + # 输出到文件 + python3 bmc_log_analyzer.py dump_info/ -o report.md + + # 只查看串口诊断 + python3 bmc_log_analyzer.py dump_info/ --check serial + """) + ) + parser.add_argument("path", help="日志包路径(.tar.gz 或解压后的目录)") + parser.add_argument("-o", "--output", help="输出报告文件路径(默认打印到 stdout)") + parser.add_argument("--check", choices=["all", "serial", "network", "errors", "version"], + default="all", help="检查范围(默认 all)") + args = parser.parse_args() + + # 解压(如需要) + base_path = extract_if_needed(args.path) + + # 找到 dump_info 根目录 + root = find_dump_root(base_path) + if not root: + print(f"[ERROR] 无法在 '{base_path}' 中找到有效的 BMC 日志目录结构", file=sys.stderr) + print(f"[ERROR] 预期找到包含 RTOSDump/ 或 LogDump/ 的目录", file=sys.stderr) + sys.exit(1) + + print(f"[INFO] 找到日志根目录: {root}", file=sys.stderr) + pkg_name = Path(args.path).name + + # 生成报告 + report = generate_report(root, pkg_name) + + # 输出 + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(report) + print(f"[INFO] 报告已保存到: {args.output}", file=sys.stderr) + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/skills/infrastructure/bmc-analyze/bmc_log_mcp_server.py b/skills/infrastructure/bmc-analyze/bmc_log_mcp_server.py new file mode 100644 index 0000000..1c184b0 --- /dev/null +++ b/skills/infrastructure/bmc-analyze/bmc_log_mcp_server.py @@ -0,0 +1,627 @@ +#!/usr/bin/env python3 +""" +BMC 日志分析 MCP Server +遵循 MCP (Model Context Protocol) 规范,为 Claude 提供 BMC 日志分析工具 + +使用方式: + # 在 Claude Code 的 settings.json 中配置: + { + "mcpServers": { + "bmc-log-analyzer": { + "command": "python3", + "args": ["/path/to/bmc_log_mcp_server.py"] + } + } + } + +可用工具: + - analyze_log_package : 全量分析日志包,返回摘要报告 + - get_version_info : 获取 BMC 版本信息 + - get_network_info : 获取网络配置 + - check_serial_config : 专项检查串口/telnet 配置(帖子#4257 的核心问题) + - search_errors : 搜索错误日志 + - list_log_structure : 列出日志包目录结构 + - read_log_file : 读取指定日志文件内容 +""" + +import json +import sys +import os +import re +import gzip +from pathlib import Path + + +# ─── 复用分析库 ────────────────────────────────────────────────────────────── + +# 在同一目录下导入分析模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from bmc_log_analyzer import ( + find_dump_root, + analyze_version, + analyze_network, + analyze_serial_config, + analyze_errors, + generate_report, + read_file_safe, + KEY_FILES, +) + + +# ─── MCP 协议实现(stdio 模式)─────────────────────────────────────────────── + +def send_response(response: dict): + """向 stdout 发送 JSON-RPC 响应""" + line = json.dumps(response, ensure_ascii=False) + sys.stdout.write(line + "\n") + sys.stdout.flush() + + +def send_error(req_id, code: int, message: str): + send_response({ + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": code, "message": message} + }) + + +def send_result(req_id, result): + send_response({ + "jsonrpc": "2.0", + "id": req_id, + "result": result + }) + + +# ─── 工具定义 ──────────────────────────────────────────────────────────────── + +TOOLS = [ + { + "name": "analyze_log_package", + "description": ( + "全量分析 OpenUBMC/iBMC 日志包,生成结构化 Markdown 摘要报告。\n" + "报告包含:BMC版本、IP地址、串口配置状态、错误日志统计、诊断建议。\n" + "适用于:用户上传日志包后的首次快速分析。" + ), + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "日志包路径:可以是 .tar.gz 文件或已解压的目录路径" + } + }, + "required": ["path"] + } + }, + { + "name": "get_version_info", + "description": "获取 BMC 版本信息,包括活跃版本号、备份版本号、产品型号、BIOS版本等。", + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"} + }, + "required": ["path"] + } + }, + { + "name": "get_network_info", + "description": "获取网络配置信息,包括IP地址、网口状态、活跃端口、防火墙规则数量。", + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"} + }, + "required": ["path"] + } + }, + { + "name": "check_serial_config", + "description": ( + "专项检查 BMC 串口和 telnet 配置。\n" + "这是 OpenUBMC 论坛帖子 #4257 中发现的典型故障场景:\n" + "用户反馈 BMC 无法通过 telnet 登录,根因是串口服务未配置或未启动。\n" + "该工具会检查:telnetd 进程状态、systemd 服务状态、操作日志中的串口操作记录。" + ), + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"} + }, + "required": ["path"] + } + }, + { + "name": "search_errors", + "description": "在日志包中搜索错误信息,支持关键词过滤和日志类型选择。", + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"}, + "keyword": { + "type": "string", + "description": "搜索关键词(可选,不填则搜索所有ERROR)", + "default": "" + }, + "log_type": { + "type": "string", + "description": "日志类型:app、maintenance、dmesg、operation、all", + "enum": ["app", "maintenance", "dmesg", "operation", "all"], + "default": "all" + }, + "max_results": { + "type": "integer", + "description": "最多返回条数", + "default": 50 + } + }, + "required": ["path"] + } + }, + { + "name": "list_log_structure", + "description": ( + "列出日志包的目录结构,并标注每个关键文件的用途。\n" + "帮助用户理解'哪个目录放哪类日志'。" + ), + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"}, + "depth": { + "type": "integer", + "description": "目录树显示深度(默认3层)", + "default": 3 + } + }, + "required": ["path"] + } + }, + { + "name": "read_log_file", + "description": ( + "读取日志包中指定文件的内容。\n" + "支持读取 .gz 压缩文件。可指定读取行数范围。" + ), + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "日志包路径"}, + "file_path": { + "type": "string", + "description": "要读取的文件路径(相对于 dump_info/ 根目录)" + }, + "head": { + "type": "integer", + "description": "读取前N行(0表示全部)", + "default": 100 + }, + "grep": { + "type": "string", + "description": "过滤关键词(可选)", + "default": "" + } + }, + "required": ["path", "file_path"] + } + } +] + + +# ─── 工具实现 ──────────────────────────────────────────────────────────────── + +def _get_root(path_str: str) -> Path: + """获取日志根目录""" + from bmc_log_analyzer import extract_if_needed + base = extract_if_needed(path_str) + root = find_dump_root(base) + if not root: + raise ValueError(f"无法在 '{path_str}' 中找到有效的 BMC 日志结构(缺少 RTOSDump/ 或 LogDump/)") + return root + + +def tool_analyze_log_package(params: dict) -> str: + path = params["path"] + root = _get_root(path) + report = generate_report(root, Path(path).name) + return report + + +def tool_get_version_info(params: dict) -> str: + root = _get_root(params["path"]) + ver = analyze_version(root) + lines = [ + f"## BMC 版本信息", + f"", + f"- **BMC类型**: {ver['bmc_type']}", + f"- **活跃版本**: {ver['bmc_version']} (Build {ver['bmc_build']})", + f"- **编译时间**: {ver['bmc_built']}", + f"- **备份版本**: {ver['backup_version']}", + f"- **产品型号**: {ver['product_name']}", + f"- **BIOS版本**: {ver['bios_version']}", + f"", + f"**原始内容(app_revision.txt 前30行):**", + f"```", + ver["raw"][:1500], + f"```", + ] + return "\n".join(lines) + + +def tool_get_network_info(params: dict) -> str: + root = _get_root(params["path"]) + net = analyze_network(root) + lines = [ + f"## 网络配置信息", + f"", + f"- **BMC IP**: {net['bmc_ip']}", + f"- **活跃端口**: {net['active_port']}", + f"- **网络模式**: {net['net_mode']}", + f"- **telnet端口(23)监听**: {'是' if net.get('telnet_port_open') else '否'}", + f"- **防火墙规则数**: {net['firewall_rules_count']}", + f"", + f"**网络接口:**", + ] + for iface in net["interfaces"]: + lines.append(f"- {iface['name']}: IP={iface.get('ip', '-')} MAC={iface.get('mac', '-')} 状态={iface.get('state', '-')}") + if net["listening_ports"]: + lines.append(f"") + lines.append(f"**监听端口**: {sorted(net['listening_ports'])}") + return "\n".join(lines) + + +def tool_check_serial_config(params: dict) -> str: + root = _get_root(params["path"]) + serial = analyze_serial_config(root) + lines = [ + f"## 串口/Telnet 配置检查", + f"", + f"### 状态汇总", + f"", + f"| 检查项 | 结果 |", + f"|-------|------|", + f"| telnetd 进程 | {'✓ 运行中' if serial['telnetd_running'] else '✗ 未运行'} |", + f"| systemd 服务状态 | {serial['telnetd_service_state']} |", + f"", + f"### 诊断结论", + f"", + ] + for d in serial["diagnosis"]: + lines.append(f"- {d}") + + if serial["telnetd_process"]: + lines.append(f"") + lines.append(f"### telnetd 进程详情") + lines.append(f"```") + lines.append(serial["telnetd_process"]) + lines.append(f"```") + + if serial["serial_operations"]: + lines.append(f"") + lines.append(f"### 操作日志:串口相关操作(最近5条)") + lines.append(f"```") + for op in serial["serial_operations"][-5:]: + lines.append(op[:300]) + lines.append(f"```") + + if hasattr(serial, "get") and serial.get("telnetd_last_event"): + lines.append(f"") + lines.append(f"### systemd 最后一条 telnetd 事件") + lines.append(f"```") + lines.append(serial.get("telnetd_last_event", "")) + lines.append(f"```") + + lines.append(f"") + lines.append(f"### 相关文件") + lines.append(f"- `AppDump/bmc_network/network_info.txt` - BMC 网络及串口配置") + lines.append(f"- `RTOSDump/networkinfo/netstat_info` - 端口监听状态") + lines.append(f"- `RTOSDump/sysinfo/journalctl.log` - 服务启停记录") + lines.append(f"- `LogDump/operation.log` - 用户操作审计(含串口操作)") + + return "\n".join(lines) + + +def tool_search_errors(params: dict) -> str: + root = _get_root(params["path"]) + keyword = params.get("keyword", "") + log_type = params.get("log_type", "all") + max_results = params.get("max_results", 50) + + # 确定要搜索的日志文件 + log_targets = { + "app": [("LogDump/app.log", "app.log")], + "maintenance": [("LogDump/maintenance.log", "maintenance.log")], + "dmesg": [("RTOSDump/driver_info/dmesg_info", "dmesg")], + "operation": [("LogDump/operation.log", "operation.log")], + "all": [ + ("LogDump/app.log", "app.log"), + ("LogDump/maintenance.log", "maintenance.log"), + ("RTOSDump/driver_info/dmesg_info", "dmesg"), + ("LogDump/operation.log", "operation.log"), + ("RTOSDump/sysinfo/journalctl.log", "journalctl"), + ] + } + targets = log_targets.get(log_type, log_targets["all"]) + + results = [] + for rel_path, label in targets: + full_path = root / rel_path + if not full_path.exists(): + continue + content = read_file_safe(full_path) + for i, line in enumerate(content.split("\n"), 1): + # 错误行过滤 + is_error = any(kw in line for kw in ["ERROR", "FATAL", "error:", "WARN"]) + # 关键词过滤 + if keyword: + if keyword.lower() not in line.lower(): + continue + elif not is_error: + continue + + results.append(f"[{label}:{i}] {line.strip()}") + if len(results) >= max_results: + break + if len(results) >= max_results: + break + + lines = [ + f"## 错误日志搜索结果", + f"", + f"- **关键词**: {keyword or '(所有ERROR)'}", + f"- **日志范围**: {log_type}", + f"- **找到**: {len(results)} 条(最多显示 {max_results} 条)", + f"", + f"```", + ] + lines.extend(results[:max_results]) + lines.append(f"```") + return "\n".join(lines) + + +def tool_list_log_structure(params: dict) -> str: + root = _get_root(params["path"]) + max_depth = params.get("depth", 3) + + # 关键目录说明 + dir_descriptions = { + "RTOSDump": "操作系统层信息(网络、驱动、系统状态)", + "RTOSDump/versioninfo": "版本信息(★首要检查)", + "RTOSDump/networkinfo": "网络接口信息(IP、路由、防火墙)", + "RTOSDump/driver_info": "内核驱动信息(dmesg)", + "RTOSDump/sysinfo": "系统状态(内存、进程、journalctl)", + "RTOSDump/other_info": "其他配置(SSH、NTP、命令历史)", + "LogDump": "应用层日志(★核心日志目录)", + "AppDump": "各子系统专项Dump", + "AppDump/bmc_network": "BMC网络配置(★串口配置在此)", + "AppDump/fault_diagnosis": "故障诊断数据", + "AppDump/bmc_health": "BMC健康状态", + "AppDump/sensor": "传感器数据", + "AppDump/power_mgmt": "电源管理", + "AppDump/cooling": "风扇/温控", + "CoreDump": "进程崩溃coredump文件", + "BMALogDump": "BMA(主机侧BMC代理)日志", + "OSDump": "主机OS信息", + } + + # 文件说明 + file_descriptions = { + "app_revision.txt": "★ BMC版本号(首要查看)", + "ifconfig_info": "★ 网口IP地址", + "network_info.txt": "★ BMC网络和串口配置", + "app.log": "★ 应用层错误日志(默认ERROR级)", + "operation.log": "用户操作审计日志", + "alarm.log": "告警事件日志", + "maintenance.log": "维护任务日志", + "dmesg_info": "内核日志(驱动错误)", + "journalctl.log": "systemd服务日志", + "ash_history": "管理员命令历史", + "sshd_config": "SSH配置", + "netstat_info": "端口监听状态", + "ps_info": "当前进程列表", + } + + def tree(path: Path, prefix: str = "", depth: int = 0) -> list: + if depth > max_depth: + return [] + lines = [] + try: + items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name)) + except PermissionError: + return [] + + for i, item in enumerate(items): + is_last = (i == len(items) - 1) + connector = "└── " if is_last else "├── " + ext = " " if is_last else "│ " + + # 获取说明 + rel = str(item.relative_to(root)) + desc = "" + if item.is_dir(): + desc = dir_descriptions.get(rel, "") + else: + desc = file_descriptions.get(item.name, "") + + desc_str = f" ← {desc}" if desc else "" + lines.append(f"{prefix}{connector}{item.name}{desc_str}") + + if item.is_dir() and depth < max_depth: + lines.extend(tree(item, prefix + ext + " ", depth + 1)) + return lines + + result_lines = [ + f"## 日志包目录结构", + f"", + f"根目录: `{root}`", + f"", + f"```", + f"dump_info/", + ] + result_lines.extend(tree(root)) + result_lines.append(f"```") + result_lines.append(f"") + result_lines.append(f"## 关键文件快速参考") + result_lines.append(f"") + result_lines.append(f"| 查找什么 | 文件 |") + result_lines.append(f"|---------|------|") + key_refs = [ + ("BMC版本", "RTOSDump/versioninfo/app_revision.txt"), + ("BMC IP地址", "RTOSDump/networkinfo/ifconfig_info"), + ("串口/telnet配置", "AppDump/bmc_network/network_info.txt"), + ("应用错误日志", "LogDump/app.log"), + ("操作审计", "LogDump/operation.log"), + ("内核错误", "RTOSDump/driver_info/dmesg_info"), + ("服务状态", "RTOSDump/sysinfo/journalctl.log"), + ("进程列表", "RTOSDump/sysinfo/ps_info"), + ("命令历史", "RTOSDump/other_info/command_records/Administrator/ash_history"), + ] + for what, where in key_refs: + exists = "✓" if (root / where).exists() else "✗" + result_lines.append(f"| {what} | `{where}` {exists} |") + + return "\n".join(result_lines) + + +def tool_read_log_file(params: dict) -> str: + root = _get_root(params["path"]) + file_path = params["file_path"] + head = params.get("head", 100) + grep_kw = params.get("grep", "") + + full_path = root / file_path + if not full_path.exists(): + return f"文件不存在: `{file_path}`\n\n根目录: {root}" + + content = read_file_safe(full_path, max_bytes=500000) + lines_all = content.split("\n") + + # 关键词过滤 + if grep_kw: + filtered = [l for l in lines_all if grep_kw.lower() in l.lower()] + else: + filtered = lines_all + + # head 截取 + if head > 0: + display = filtered[:head] + truncated = len(filtered) > head + else: + display = filtered + truncated = False + + result = [ + f"## 文件内容:`{file_path}`", + f"", + f"- **文件大小**: {full_path.stat().st_size} bytes", + f"- **总行数**: {len(lines_all)}", + ] + if grep_kw: + result.append(f"- **过滤关键词**: `{grep_kw}`,匹配 {len(filtered)} 行") + if truncated: + result.append(f"- **显示**: 前 {head} 行(共 {len(filtered)} 行)") + result.append(f"") + result.append(f"```") + result.extend(display) + result.append(f"```") + + return "\n".join(result) + + +# ─── 工具路由 ──────────────────────────────────────────────────────────────── + +TOOL_HANDLERS = { + "analyze_log_package": tool_analyze_log_package, + "get_version_info": tool_get_version_info, + "get_network_info": tool_get_network_info, + "check_serial_config": tool_check_serial_config, + "search_errors": tool_search_errors, + "list_log_structure": tool_list_log_structure, + "read_log_file": tool_read_log_file, +} + + +# ─── MCP 主循环 ────────────────────────────────────────────────────────────── + +def handle_request(request: dict): + req_id = request.get("id") + method = request.get("method", "") + params = request.get("params", {}) + + # 初始化握手 + if method == "initialize": + send_result(req_id, { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": { + "name": "bmc-log-analyzer", + "version": "1.0.0" + } + }) + return + + if method == "notifications/initialized": + return # 无需响应 + + # 列举工具 + if method == "tools/list": + send_result(req_id, {"tools": TOOLS}) + return + + # 调用工具 + if method == "tools/call": + tool_name = params.get("name", "") + tool_params = params.get("arguments", {}) + + handler = TOOL_HANDLERS.get(tool_name) + if not handler: + send_error(req_id, -32601, f"未知工具: {tool_name}") + return + + try: + result_text = handler(tool_params) + send_result(req_id, { + "content": [ + {"type": "text", "text": result_text} + ] + }) + except Exception as e: + send_result(req_id, { + "content": [ + {"type": "text", "text": f"工具执行错误: {str(e)}"} + ], + "isError": True + }) + return + + send_error(req_id, -32601, f"未知方法: {method}") + + +def main(): + """MCP Server 主循环(stdio 模式)""" + print(f"BMC Log Analyzer MCP Server 已启动 (stdio 模式)", file=sys.stderr) + + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + request = json.loads(line) + except json.JSONDecodeError as e: + send_response({ + "jsonrpc": "2.0", + "id": None, + "error": {"code": -32700, "message": f"JSON 解析错误: {e}"} + }) + continue + + try: + handle_request(request) + except Exception as e: + req_id = request.get("id") + send_error(req_id, -32603, f"内部错误: {str(e)}") + + +if __name__ == "__main__": + main()