简介:在IT运维中,H3C交换机巡检常受制于手动登录、逐台执行命令的低效方式。针对这一场景,Python自动化巡检的实现详解从netmiko连接配置讲起,逐步演示通过SSH批量登录交换机,执行display ip routing-table、dis version、display fan、display cpu-usage、display memory等命令,并将返回结果按设备写入文本。随后使用xlwt把文本转为Excel表格,并借助smtplib发送邮件或钉钉机器人推送报告,最终配合Linux crond实现定期无人值守巡检。全部内容浓缩为1个PDF文件,大小33KB,篇幅精简但流程完整、代码可复用。已有4413人学习/下载,适合有Python基础且需维护多台H3C设备的运维人员,可有效减少重复操作、降低漏检风险、提升排障效率。
1. Python自动巡检H3C交换机:从手工登录到脚本批量拉回状态
早几年我还在某公司做网络运维时,每天早上最枯燥的事就是登录几十台H3C交换机,逐台敲display cpu、display memory、display interface brief,再把屏幕上滚动的字符复制进一个带日期的文本文件里。遇到设备多了或者登录卡顿,一上午就这么没了,还总担心漏掉哪一台。后来我用 Python 写了套自动巡检脚本,把登录、取数、初判都交给程序完成,我只需要在上班后打开一份汇总报告。这篇笔记就把这个实现过程拆开讲:怎么选协议、怎么封装登录、怎么解析回显、有哪些坑必须提前躲开。它不只适合网络工程师,也适合正在做运维自动化的开发同学,因为 H3C 设备足够典型,这套思路换到其他厂商设备同样成立。
2. 巡检H3C交换机前先定三件事:协议、资产表、运行环境
动手写脚本之前,如果直接跳到连接代码,大概率会在批量执行时翻车。我先定清楚三件事:用 SSH 还是 SNMP 拿数据、设备清单怎么维护、脚本跑在什么环境里。这三件事没想明白,后面每调一次都要改结构。
2.1 为什么我最终用 SSH + Netmiko 而不是 SNMP
一提到网络设备采集,很多人第一反应是 SNMP 轮询。SNMP 的好处是标准、轻量、适合监控系统持续拉取,但放到我这个巡检场景里有个明显短板:我只能拿到 OID 定义好的数据,比如接口流量、CPU 均值,可我想顺带看设备版本、告警日志、端口物理状态,这些内容很多不在标准 MIB 里,或者需要在设备上做额外映射。另外一个实际问题:当时设备上的 SNMP 配置并不统一,有人开了 v2c,有人只开 v3,还有的 community 串连自己都不清楚。操作用户名密码登录就简单得多,只要运维账号没被禁用,所有状态都能通过命令行拿到。
所以我选了 SSH 通道,而且没有直接用 Paramiko,而是在它之上用了 Netmiko。Paramiko 只提供 SSH 会话能力,分页处理、提示符等待、命令回显清理都要自己写;Netmiko 把这些通用逻辑封装好了,还针对不同厂商设备定义了不同行为,H3C 设备在它的设备类型表里有专门条目,直接指定device_type="h3c"就能用。有人会问,H3C 和 HPE Comware 是不是同一个设备类型?早期版本里 netmiko 对两者处理有差异,实际使用中我更喜欢单独写h3c,避免提示符匹配规则不对导致命令发不出去。
安装依赖也简单,一个 netmiko 就能覆盖 SSH 连接和命令交互。我在干净的虚拟环境里执行:
pip install netmiko安装完成后可以用下面这行快速验证库是否就位,不输出错误就说明环境没问题。
python -c "import netmiko; print(netmiko.__version__)"需要注意的是,我这边没有额外安装 paramiko,因为 netmiko 会自动把它带进来。如果你所在的办公网对外网受限,离线安装时记得把 netmiko 的依赖一起下载,常见依赖包括 paramiko、scp、textfsm、ntc-templates 等,具体列表以你使用的 netmiko 版本为准。
2.2 把设备资产表整理成 CSV,少填一个字段后面就多花一小时
H3C 设备的登录参数虽然是固定的几项,但批量脚本运行的前提是有一份可靠的设备清单。我见过有人在 Python 脚本里硬编码一个列表devices = [{"ip": "192.168.1.1", ...}],这种做法在小规模验证时没问题,等设备增加到几十台,修改脚本就要重新部署,还容易把账号密码提交到版本库。我的习惯是把设备信息抽到一个独立 CSV 文件里,脚本只负责读它。
资产表至少应该包含这几列:ip、username、password、port、device_type。如果有人用不同管理账号访问不同设备,也可以加一列account_group,但初期不建议把模型设计得太复杂,五列够用。
下面是我当时使用的devices.csv示例:
ip,username,password,port,device_type 192.168.10.11,ops_admin,Str0ngP@ss,22,h3c 192.168.10.12,ops_admin,Str0ngP@ss,22,h3c 192.168.10.13,netops,Another@Pass123,22,h3c读取 CSV 时我建议直接用 Python 标准库csv,不要为了这一步引入 pandas。网络设备数量级通常在几十到几百台,标准库足够,还能减少环境依赖。下面这段代码会把每一行转成字典,并顺便过滤掉空行:
import csv def load_devices(path): devices = [] with open(path, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: if row.get('ip'): devices.append(row) return devices if __name__ == '__main__': for d in load_devices('devices.csv'): print(d['ip'], d['device_type'])这段代码的逻辑很简单:用DictReader读取,表头自动变成字典的键,循环里只追加非空设备行。关于密码字段有一点要注意,如果密码里包含逗号或引号,CSV 必须用双引号包起来,否则行会被拆错。我在项目里曾经因为某个密码里带了#没出问题,但换成一个包含逗号的密码后,脚本怎么都对不上设备,排查半天才发现是 CSV 解析错了。
2.3 运行环境与依赖:Python3.6+,一个 Netmiko 库够用
运行环境上,我在两台机器都跑过这套脚本:一台是 Windows 10 办公电脑,一台是 CentOS 7 服务器。Python 版本至少需要 3.6,因为 netmiko 使用了一些新语法,3.6 以下容易出现兼容问题。我现在习惯用 3.8 及以上,主要是正则和字符串处理更顺手,不过没必要为了这个脚本单独升级生产环境。
另外,确保网络能达到目标设备的 TCP 22 端口。很多办公室规定了运维网段白名单,脚本执行机如果不是从运维网段发出连接,第一步就会卡在超时上。我的做法是在脚本里先对 IP 做一次socket连通性预检,快速跳过不可达设备,避免后面每台设备都等连接超时。下面这段探测代码可以放在批量循环前:
import socket def is_reachable(ip, port=22, timeout=3): try: with socket.create_connection((ip, port), timeout=timeout): return True except OSError: return False这个预检不是万能的,但它能把网络层的“连不上”和账号层的“认证失败”区分开,日志里报错会清晰很多。我一般会给它 3 秒超时,因为一台设备不可达时,等待 3 秒比等 TCP 默认超时快得多。整个脚本只依赖 netmiko 和标准库,不用弄一堆工具链,也方便其他人接手。
3. 用Netmiko连接H3C交换机并批量执行命令:最小可用代码与参数调整
到了这一步,基础条件已经准备好,接下来就是真正写连接逻辑。很多人一开始就试图写一个包含所有功能的“大函数”,结果越来越难调。我建议先写一个单设备连接的骨架,确认能跑通,再扩展成批量版本。
3.1 单台连接的骨架:三步走(连接、关分页、执行命令)
连接 H3C 交换机的最小代码如下,我把它放在connect_one.py里:
from netmiko import ConnectHandler device = { "device_type": "h3c", "ip": "192.168.10.11", "username": "ops_admin", "password": "Str0ngP@ss", "port": 22, "timeout": 15, "global_delay_factor": 2, } dev_conn = ConnectHandler(**device) # 关闭临时分页,避免命令回显被截断 dev_conn.send_command("screen-length 0 temporary") # 关闭终端日志推送,防止告警信息插在命令回显里 dev_conn.send_command("undo terminal monitor") output = dev_conn.send_command("display cpu") print(output) dev_conn.disconnect()这里ConnectHandler接收的是连接参数字典,**device将字典展开成关键字参数。device_type决定了 netmiko 用哪套提示符匹配规则和命令发送方式,H3C 设备用h3c。timeout是连接阶段的等待时间,我设置 15 秒是因为有些老设备 SSH 服务启动慢。global_delay_factor是 netmiko 里的通用延时放大倍数,所有读写操作的默认等待时间都会乘以这个值,H3C 设备 CPU 性能弱,设为 2 能明显降低空回显概率。
连接成功后,send_command会发送命令并等待提示符重新出现。注意我特意先发了screen-length 0 temporary和undo terminal monitor,这两条不是可有可无。前者把当前会话的分页长度设为 0,让命令回显一次全部输出;后者关闭终端监视器,防止端口 up/down、日志等异步消息混入回显里。很多人只发第一条,忘记关日志,后面解析时就会看到奇怪的行。
3.2 H3C登录慢和回显分页:两个影响稳定性的参数
H3C 设备,尤其是老款交换机,SSH 登录初始化和命令回显速度明显比新设备慢。如果使用 netmiko 默认参数,很容易出现Read timeout或者日志中报“找不到提示符”。除了前面说的global_delay_factor,还有fast_cli这个参数值得说明一下。fast_cli=True会让 netmiko 关闭verbose输出并尝试减少多余等待,但不要盲目开启,因为它在慢速 H3C 设备上反而会降低成功率。我的建议是:默认先不设置fast_cli,遇到明显卡顿的设备再单独调。
另一个问题是命令回显分页。即使发送了screen-length 0 temporary,在某些 Comware 版本上,这条命令本身可能不在当前管理员权限下可用。如果发送时报权限错误,不必中断,可以继续发screen-length disable试试,或者把display命令改成带| no-more的形式。实际上 H3C 的display命令支持管道符,例如display cpu | no-more,这也是跳过临时配置的一个办法。我在脚本里的处理是优先用临时配置方案,因为不同版本对| no-more的支持并不一致。
还有一个容易被忽略的细节:send_command默认会启用自动分页处理?其实 netmiko 对 h3c 设备常用配置里会发送分页禁用命令,但为了兼容性和可控性,我自己显式发一次更安心。而且显式发送还有一个好处,后续同一会话再执行长输出时不会有分页残留问题。
3.3 从单台到批量:循环里如何处理会话与异常
连接一台设备成功之后,批量就是循环加异常处理的活。批量脚本要保证一台设备出错不能拖垮整体,所以try/except是必须的。下面是我常用的批量执行模板,它会把每台设备每个命令的输出分别存到以 IP 命名的文本文件里:
from netmiko import ConnectHandler from concurrent.futures import ThreadPoolExecutor, as_completed import csv import os def inspect_single(device): try: conn = ConnectHandler(**device) conn.send_command("screen-length 0 temporary") conn.send_command("undo terminal monitor") result = {} for cmd in ["display cpu", "display memory", "display interface brief"]: result[cmd] = conn.send_command(cmd) conn.disconnect() return device["ip"], result, None except Exception as err: return device["ip"], None, str(err) def load_devices(path): devices = [] with open(path, newline='', encoding='utf-8') as f: for row in csv.DictReader(f): if row.get("ip"): row["timeout"] = 15 row["global_delay_factor"] = 2 devices.append(row) return devices if __name__ == "__main__": devices = load_devices("devices.csv") success_count = 0 with ThreadPoolExecutor(max_workers=10) as pool: futures = {pool.submit(inspect_single, d): d for d in devices} for fut in as_completed(futures): ip, result, err = fut.result() if err: print(f"{ip} 巡检失败: {err}") else: success_count += 1 os.makedirs("results", exist_ok=True) with open(f"results/{ip}.txt", "w", encoding="utf-8") as f: for cmd, output in result.items(): f.write(f"### {cmd}\n") f.write(output) f.write("\n") print(f"完成,成功 {success_count}/{len(devices)}")这里用了ThreadPoolExecutor做并发,因为 SSH 连接是 IO 密集型的,串行跑 30 台设备可能要半小时,并发 10 线程能把时间压缩到几分钟。max_workers=10是个比较保守的值,既不会因为线程太多压垮设备管理 CPU,也能明显提速。每个线程里inspect_single独立处理一台设备,内部用异常捕获隔离错误,即使某一台认证失败,进程也会继续执行后续设备。运行结束后,results目录下会出现以 IP 命名的文件,每个文件里按命令分段保存了原始回显。
需要提醒的是,ConnectHandler创建会话后用disconnect()关闭是必要的。如果某个异常发生在disconnect()之前,连接对象会在垃圾回收时释放,但自动释放时机不可控,长时间运行会堆积大量半开连接。更好的做法是用with上下文管理器,不过 netmiko 的历史版本对上下文支持有差异,所以我这里选择了显式断开的写法,至少在异常路径上已经打印了错误,不会静默泄漏。
4. 把display回显解析成结构化数据:正则提取CPU、内存与端口状态
巡检不只为了把原始回显存档,更重要的是让程序能判断设备是否健康。如果每次都要人工打开文本文件看 CPU,自动化的意义就少了一半。所以下一步是把命令回显切成能入库、能对比的字段。
4.1 巡检项对应的H3C命令清单
H3C Comware 系统里有几个经典巡检命令,不同版本回显格式略有差异,但关键字段高度一致。我把常用的整理成一张表,脚本里按需增减。
| 巡检目标 | display 命令 | 关键字段 |
|---|---|---|
| CPU 占用率 | display cpu | CPU usage、slot 编号 |
| 内存占用率 | display memory | Memory Usage、Total/Free |
| 接口状态 | display interface brief | Link 状态为 UP/DOWN、速率、Duplex |
| 设备温度 | display environment | 当前温度、告警阈值 |
| 系统版本与运行时间 | display version | Software Version、uptime |
| 设备硬件状态 | display device | Status 为 Normal/Fault |
建议不要一开始就采集所有命令,先选 CPU、内存、接口状态这三项。解析逻辑稳定后再加温度和设备状态,因为温度回显在虚拟化平台和实体机上差异较大,设备型号不同字段名也不同,过早收集会让第一版脚本卡在解析兼容性上。
4.2 用正则提取CPU和内存的数值
H3C 的display cpu回显通常长这样:
Slot 1 CPU 0: CPU 0 (Slot 1) current state : Normal CPU usage: 4.5%不同型号可能在前面加上DRAM或runtime标识,但真正要抓的是4.5%这个数字。用正则提取时要注意两点:数字是浮点数,百分号后可能有空格或换行;另外设备只有一块主控时,回显里也可能没有 Slot 信息,正则要兼容两种情况。我写的提取函数如下:
import re def parse_cpu(text): # 匹配类似 "CPU usage: 4.5%" 的片段 m = re.search(r"CPU\s+usage[^0-9]*([0-9]{1,2})(?:\.[0-9])?", text, re.IGNORECASE) if m: return float(m.group(1)) # 兼容 "CPU 0 (Slot 1) current state : Normal" 这种无百分比的回显 m2 = re.search(r"current state\s*:\s*(\w+)", text, re.IGNORECASE) if m2: state = m2.group(1).lower() if state == "normal": return 0 return None这个函数里的第一个正则有瑕疵,([0-9]{1,2})(?:\.[0-9])?只能匹配一位小数,而且没有把小数点前整数和后面小数组合成一个合法浮点数。实际跑下来发现,H3C 有的版本显示4.5%,有的显示4%,所以我改成捕获完整数字更稳妥:
def parse_cpu(text): m = re.search(r"CPU\s+usage\s*[::]?\s*([0-9]{1,3}(?:\.[0-9]{1,2})?)\%", text, re.IGNORECASE) if m: return float(m.group(1)) return None这里把百分号也放进匹配范围,避免误匹配到描述文本里的其他数字。内存解析思路相同,display memory回显通常有类似Memory Used: 8947840 KB、Memory Free: 12977456 KB或者直接给百分比。我会同时抓已用和空闲,计算使用率:使用率 = 已用 / (已用 + 空闲)。直接抓百分比字段省事,但有些版本不直接给百分比,所以用两个字段自己算更通用。
def parse_memory(text): used = re.search(r"Memory Used[^0-9]*([0-9]+)", text, re.IGNORECASE) free = re.search(r"Memory Free[^0-9]*([0-9]+)", text, re.IGNORECASE) if used and free: used_kb = float(used.group(1)) free_kb = float(free.group(1)) return used_kb / (used_kb + free_kb) * 100 return None正则解析这类文本最大的风险不是写错模式,而是设备回显里混入了转义字符。比如有些设备开启了彩色终端,回显里会有形如\x1b[0m的 ANSI 转义码,肉眼看不出来,但正则匹配时会被打断。这里推荐在进入解析函数前先用一个清理函数把 ANSI 控制字符剥掉:
def strip_ansi(text): ansi_re = re.compile(r"\x1b\[[0-9;]*[mK]") return ansi_re.sub("", text)调用时先text = strip_ansi(text)再交到解析函数,能省掉很多玄学问题。
4.3 解析失败别硬扛:保存原始回显作为兜底
正则再怎么兼容,也挡不住设备版本升级后回显格式大变。我第一版脚本上线后,就遇到过某型号交换机升级固件后display memory字段从英文变成了中文,正则全部匹配不到。这时候如果脚本直接返回None,后续做阈值判断就会把这个设备当作“数据缺失”处理,既不准确也不直观。
我养成的习惯是:每一台设备的原始回显一定要落盘保存,解析成功与否都保留。前面批量脚本已经写了results/{ip}.txt,解析完成后我再生成一个parsed_{ip}.json,里面是字段名和解析结果。如果某个字段解析失败,在 JSON 里写成null,同时写一条日志到warnings.log。这样出了问题,我既能拿到原始数据二次分析,也能在报告里看到哪个设备哪一项数据缺失。
import json def parse_and_dump(ip, raw_outputs, parsed_path="parsed_results"): parsed = {} os.makedirs(parsed_path, exist_ok=True) for cmd, output in raw_outputs.items(): cleaned = strip_ansi(output) if "cpu" in cmd: parsed["cpu"] = parse_cpu(cleaned) elif "memory" in cmd: parsed["memory"] = parse_memory(cleaned) else: parsed.setdefault(cmd, None) with open(f"{parsed_path}/{ip}.json", "w", encoding="utf-8") as f: json.dump(parsed, f, ensure_ascii=False, indent=2)这个兜底思路比精确解析更重要。哪怕解析代码写得很完美,也会遇到设备响应超时、接口状态字段拼接等边缘情况。保留原始回显,就等于给每条数据买了后悔药。
5. 自动巡检H3C交换机的避坑指南:5个让我白天想睡觉的坑
脚本能跑不等于稳定,更不等于可以完全放开不管。下面这几个坑我都是在真实环境里踩过以后,才慢慢摸索出解决办法的,每一条都值得提前处理。
5.1 现象:回显只有第一页,后面的数据不见了
原因:H3C 设备默认分页显示长输出,执行display interface brief这类命令时,输出超过一屏就暂停,等待用户按键。netmiko 即使一直在读,也只会拿到当前这一屏内容,而且因为提示符没出现,send_command会一直等到超时。
解决:连接后先发送screen-length 0 temporary,把当前会话的分页禁用掉。如果这条命令发送报错,再尝试screen-length disable,或者用display xxx | no-more的形式执行命令。我建议两条链路都写在代码里,第一条失败就自动切换第二条,保证高版本 Comware 也一样稳定。
5.2 现象:批量脚本卡死在某台设备上,后面所有设备都不跑了
原因:连接时代理错误或认证失败会让ConnectHandler立即抛异常,但如果设备 IP 不可达,TCP 连接会一直挂在阻塞状态;或者发送命令后设备回显异常,netmiko 等提示符等不到,默认超时时间又很长。
解决:必须给ConnectHandler传入合理的timeout,同时在每台设备的外层用try/except包裹。前面批量模板里我已经把timeout设为 15,global_delay_factor设为 2,这两项需要根据现场设备响应速度调整。如果日志显示某台设备经常超时,可以单独给这台设备加一个超时参数,而不是全局放大延时,避免拖慢整个批次。
5.3 现象:CPU使用率解析出 None,正则明明看着没问题
原因:设备回显里带有 ANSI 彩色控制符或者行尾的\r符号,正则在CPU usage:后紧跟数字时被不可见字符隔开了。我遇到过一种情况,屏幕上看是CPU usage: 4.5%,但十六进制里有\x1b[0m插在冒号和数字之间。
解决:进入解析函数前先做strip_ansi清理,并且把回显里的\r\n统一成\n。我在多个型号上对比过,清理后解析成功率从 80% 提升到了接近 100%。另外建议在解析函数里加一层防御,如果正则匹配失败,不要直接返回None,而是把原始文本片段的前 200 字符记录到日志里,方便回查。
5.4 现象:连接时控制台日志刷屏,命令回显全乱了
原因:H3C 设备默认会向登录会话推送端口状态、链路振荡等日志信息。如果有人在巡检同时动了网线,日志就会插进display cpu的回显中间,导致send_command拿到的输出包含大量无关文本,解析自然出错。
解决:连接后立即发送undo terminal monitor关闭终端日志推送。注意这条命令只影响当前会话,不会影响设备日志功能。如果某些设备提示命令不可用,可能是低版本 Comware 或用户权限不足,可以在连接参数里允许失败继续,不要让它阻断后续巡检。
5.5 现象:某个命令偶尔超时,但人工执行就正常
原因:设备 CPU 瞬时繁忙,或者当时正好有业务流量触发大量转发任务,导致 SSH 会话响应延迟。还有一种情况是 netmiko 的send_command默认等待提示符的超时时间太短,设备还没输出完命令,会话就被判定超时。
解决:给send_command单独传timeout或read_timeout参数,比如这个长命令单独用更大的等待时间。再配合一次自动重试,第一次超时后等几秒重发一次。我在脚本里对display environment这类慢命令做了重试逻辑,捕获异常后 sleep 3 秒再执行,基本能覆盖偶发延迟。
6. 让巡检脚本每天早上自动跑:定时调度与异常通知的小技巧
脚本写完、解析稳定之后,还差最后一步:让它自己跑起来。我在生产环境用 CentOS 服务器做调度,crontab 配置很简单:
30 7 * * 1-6 cd /opt/h3c_inspection && python3 run_inspection.py >> logs/cron_$(date +\%Y\%m\%d).log 2>&1每周一到周六早上 7:30 跑一次,周日留空因为那天基本不看设备状态。脚本运行完会生成summary.txt,我只需要打开看一眼成功设备和失败设备数量。如果是 Windows 办公电脑,就用任务计划程序设置触发器,命令填python.exe,参数填脚本完整路径。
为了能快速发现“少了一台设备”,我习惯在脚本最后加一段统计逻辑:对比 CSV 里的设备总数和解析 JSON 文件数量。数量对不上就说明有设备连接失败或解析中断,这时再去看日志。这个简单的数量比对,比刷几千行日志有效得多。如果要更进一步,可以把失败记录写进一个failed.txt,调度任务里再用邮件或企业微信通知模板推送,但一开始没必要为了通知功能引入额外依赖。
我在这套脚本上最大的教训是:自动化跑起来之后,一定要保留人工复核的窗口。我有一次更新了设备登录密码,脚本每天定时失败,我却因为只看汇总数字,以为“数量对不上只是偶发”,直到第三天排查才发现账号过期。后来我给自己定了个习惯:无论自动化多稳定,每天早上至少抽查一台设备的原始回显,确认命令输出内容和人眼看到的一致。这个习惯帮我提前发现过至少两次设备回显格式变更。希望这篇笔记能让你在搭自己的 H3C 自动巡检时少走弯路,跑通之后记得把原始回显留好,它会是排查时最靠得住的后路。希望帮到你。
本文还有配套的精品资源,点击获取