Shell+Expect批量备份华三交换机配置:从手动导出到自动化归档

发布时间:2026/9/15 13:14:43
Shell+Expect批量备份华三交换机配置:从手动导出到自动化归档 去年给一家工厂做网络整改现场六十多台华三交换机光是把每台设备的配置导出来归档就花了我整整两天。真到了“改错一条策略想回退”的时候你才发现自己手里根本没一份可靠的配置基线。后来我花了一个晚上写了这套批量备份华三交换机的Shell脚本——所谓“自驾”就是不依赖商业网管平台、不指望网管赏脸自己动手把这件事彻底自动化。实测下来六十多台设备跑完不到十分钟配置按“IP_日期时间”命名落盘之后每天定时执行一次再也不用手动一台台敲命令了。这套脚本用 Shell Expect 实现通过 SSH 登录设备先保存运行配置再抓取当前配置适合几十台到几百台设备量级的运维场景。没有 Python 环境也能跑拷到任何 Linux 机器上改个设备清单就能用。如果你是驻场工程师、网络管理员或者正想找真实设备练手 Shell 自动化的同学这篇可以直接拿去抄作业我也会把实际踩过的坑全部摊开讲。1. 先说清楚为什么备份配置文件比买保险还重要1.1 你大概率经历过“配置丢了”的至暗时刻网络设备跑着跑着出问题最常见也最憋屈的一种情况是有人在设备上改了几条配置改完当时看着没事过了两周业务开始抖动你想回退却发现自己压根没存过改动前的配置。这时候要么靠记忆猜要么翻聊天记录找之前谁发过一份截图要么祈祷设备本身的 startup 配置还没被覆盖。华三交换机的配置分成两层running-config当前运行配置和 startup-config启动配置文件。很多人以为设备重启后会回到“上次保存的稳定状态”但问题是运行配置和启动配置经常不一致——现场调试时敲的命令、临时加的 ACL、测试用的 VLAN只要没执行save重启之后全没了反过来如果执行过save那改动就固化进去了想回退也没有历史版本。所以配置备份不是“有空再说”的事它是网络运维里最基础也最不能省的一环。一个完整的备份机制至少要在配置变更前、变更后、每天定时这三个时间点留下配置快照。有了快照出问题时才能快速 diff才能精准回退而不是推倒重来。1.2 网管平台能备份但有三个现实问题市面上华三的网管平台、开源网管系统、商业 NMS 都能做配置备份理论上比我这个脚本强多了。但实际操作中你会发现几个现实问题一是部署成本。一套网管平台要装服务器、装数据库、配 SNMP、配 SSH 账号、学平台操作对小团队和驻场场景来说太重了。很多时候你只需要“每天把配置导出来存一份”为一个需求上一套重型系统投入产出比太低。二是设备权限和网络位置。网管平台的备份任务通常要求从服务器主动连设备如果设备在隔离网段、管理 IP 不统一、或者现场只有一台笔记本能连到设备平台就抓瞎了。而脚本可以放在笔记本上插上网线就能跑。三是平台自身也会挂。最讽刺的情况就是“网管平台崩了连带着所有历史备份都没了”。脚本把配置备份成普通文本文件放在本地磁盘或 Git 仓库里这才是真正掌握在自己手里的备份。所以我一直觉得在大平台之外手里有一份零依赖、即拷即用的批量备份脚本是网络工程师很实用的底牌。2. Shell Expect 选型为什么不用 Python 和 Ansible2.1 自用脚本的第一原则运行环境越朴素越好我见过有人为了做个备份脚本先装 Python 3、再 pip install paramiko、netmiko折腾半天环境结果换个现场机器又得重来一遍。自用工具的第一原则是什么是环境依赖越少越好。Shell 是 Linux 系统的标配Expect 在绝大多数发行版里用一条命令就能装上# Debian / Ubuntu sudo apt-get install -y expect # CentOS / RHEL sudo yum install -y expect脚本本身两个文件一个.sh一个.exp拷到任何 Linux 机器上改个设备列表就能用。Windows 上如果有 Git Bash 或者 WSL同样能跑。不需要虚拟环境不需要打包依赖不需要担心 Python 版本兼容——“能跑就行”这四字真言在运维场景里是硬道理。如果你在 Windows PowerShell 里遇到“无法将‘git’识别为 cmdlet”之类的报错那不是脚本的问题是环境不对切到 Git Bash 或 WSL 里执行即可。2.2 Expect 天然适合“陪聊式”网络设备交互华三交换机的 SSH 登录和命令执行本质是一个交互式会话你发一条命令它回一段输出你根据输出的关键字决定下一步发什么。这和人手工敲命令的流程完全一致而 Expect 干的就是这件事——它像一个陪设备聊天的人看到password就输密码看到#或就发命令看到---- More ----就发空格翻页。用 Python 的 paramiko/netmiko 当然也能做但需要写不少细节处理。Ansible 就更“重”了要装控制端、写 inventory、写 playbook虽然生态完整但对“备份配置”这一个动作来说属于杀鸡用牛刀。Expect 的另一个好处是它的执行过程是透明的出问题了你直接看屏幕上回显就能判断卡在哪一步排查效率很高。2.3 整体目录结构与执行流程我的脚本就两个核心文件加一个设备清单目录结构长这样h3c-backup/ ├── backup_h3c.sh # 主控脚本负责循环、日志、结果校验 ├── h3c_backup.exp # Expect 会话脚本负责单台设备的 SSH 交互 └── devices.txt # 设备清单IP, 用户名, 密码执行流程也不复杂主控脚本逐行读取devices.txt跳过空行和#注释行对每台设备调用一次h3c_backup.exp传入 IP、用户名、密码、输出文件路径Expect 脚本 SSH 登录设备关闭分页保存配置执行display current-configuration抓取完整配置输出文件落盘到backup/目录文件名带时间戳主控脚本检查输出文件是否为空记录成功/失败日志最后汇总统计。整个流程没有花活但胜在可靠。下面两节我把每一步的代码和原理拆开讲。3. 核心脚本拆解从 SSH 登录到配置落盘3.1 设备清单文件是全部入口devices.txt用逗号分隔三个字段一行一台设备# 格式: IP地址, SSH用户名, SSH密码 192.168.10.1,admin,H3C123456 192.168.10.2,admin,H3C123456 192.168.10.10,netadmin,Passw0rd#2024这里有两个约定。第一密码里不要包含逗号因为脚本用逗号做分隔符密码里出现逗号会把字段拆乱如果你的密码确实包含特殊字符可以把分隔符整体换成|对应把主控脚本里的IFS,改成IFS|。第二设备名也可以加进来比如192.168.10.1,SW-CORE-01,admin,H3C123456这样输出文件名能直接体现设备角色后面我会说怎么扩展。3.2 主控脚本循环调用与结果校验backup_h3c.sh的核心是一个while read循环。这里有个小细节用IFS, read -r ip user pass时我用的是-r参数防止密码或设备名里的反斜杠被转义吃掉。#!/bin/bash # 华三交换机批量配置备份脚本 # 用法: ./backup_h3c.sh [设备列表文件] [备份目录] DEV_FILE${1:-devices.txt} BACKUP_DIR${2:-./backup} LOG_FILE$BACKUP_DIR/backup_$(date %Y%m%d).log mkdir -p $BACKUP_DIR echo 批量备份开始: $(date %Y-%m-%d %H:%M:%S) | tee -a $LOG_FILE SUCCESS0 FAIL0 while IFS, read -r ip user pass; do # 跳过空行和 # 注释行 [[ -z $ip || $ip \#* ]] continue stamp$(date %Y%m%d_%H%M%S) outfile$BACKUP_DIR/${ip}_${stamp}.cfg echo ${ip} 备份中... | tee -a $LOG_FILE if /usr/bin/expect ./h3c_backup.exp $ip $user $pass $outfile $LOG_FILE 21; then # 简单校验文件非空且包含配置特征行 if [ -s $outfile ] grep -q sysname\|system-view\|return $outfile; then echo ${ip} 备份成功: $outfile | tee -a $LOG_FILE SUCCESS$((SUCCESS 1)) else echo ${ip} 备份失败: 输出文件为空或不含配置内容 | tee -a $LOG_FILE FAIL$((FAIL 1)) fi else echo ${ip} 备份失败: expect 返回非零 | tee -a $LOG_FILE FAIL$((FAIL 1)) fi done $DEV_FILE echo 备份结束: 成功 ${SUCCESS} 台, 失败 ${FAIL} 台 | tee -a $LOG_FILE主控脚本的返回值判定只是第一道关。真正判断备份是否成功要看输出文件里有没有华三配置文件的特征内容——sysname设置设备名或return配置文件结尾都是可靠标识。如果 expect 返回 0 但实际文件是空的这条校验能兜住。3.3 Expect 会话脚本交互逻辑是核心接下来是重头戏h3c_backup.exp。逐段拆解之前先把完整代码放出来#!/usr/bin/expect set timeout 20 set ip [lindex $argv 0] set user [lindex $argv 1] set pass [lindex $argv 2] set outfile [lindex $argv 3] # 登录交互过程不显示在终端也丢弃到一个临时日志 log_user 0 set login_log /tmp/h3c_login_${ip}.log log_file -noappend $login_log # 跳过主机密钥确认内网场景可接受安全要求高时可配合 known_hosts spawn ssh -o StrictHostKeyCheckingno -o UserKnownHostsFile/dev/null -o ConnectTimeout10 $user$ip expect { (yes/no) { send yes\r; exp_continue } -re (?i)password: { send $pass\r } timeout { puts LOGIN_TIMEOUT; exit 1 } eof { puts CONN_CLOSED; exit 2 } } # 等待设备提示符华三常见的是 设备名 或 [设备名] expect { -re (?n)^[\\[].*[\\]] { } timeout { puts NO_PROMPT; exit 3 } } # 关闭分页避免输出卡在 ---- More ---- send screen-length disable\r expect { -re (?n)^[\\[].*[\\]] { } timeout { puts NO_PROMPT_AFTER_SCREEN; exit 4 } } # 保存运行配置到启动配置文件确保备份的是持久化后的配置 send save\r expect { -re (?i)are you sure { send y\r; exp_continue } -re (?i)input the file name { send \r; exp_continue } -re (?n)^[\\[].*[\\]] { } timeout { puts SAVE_WARN } } # 抓取完整当前配置输出记录到独立文件 send display current-configuration\r log_file -noappend $outfile expect { -re ---- More ---- { send ; exp_continue } -re (?n)^[\\[].*[\\]] { } timeout { puts DISPLAY_TIMEOUT; exit 5 } } log_file send quit\r close exit 0这段脚本里有几个地方不是随便写的逐个说清楚。先说登录部分。StrictHostKeyCheckingno和UserKnownHostsFile/dev/null是让 SSH 不校验首次连接的主机指纹避免第一次连新设备时卡在yes/no交互。有人会担心安全问题内网管理环境一般可接受公司网络如果安全策略严格可以去掉这两行改为提前把设备指纹加入 known_hosts。再说提示符匹配。华三设备配置内容里有大量单独成行的#符号区分各配置段如果结束标志匹配成“看到 # 就结束”那display current-configuration输出到第一段就被掐断了。所以我用的是(?n)^[\\[].*[\\]]这个正则——(?n)表示开启换行敏感模式^只匹配行首也就是说只有在一行的开头出现或[且末尾是或]时才认为是设备提示符。这样配置正文里的#、sysname等字符都不会误触发。然后是save命令的兼容处理。华三 V5 和 V7 的保存逻辑略有差异V5 里执行save后会问Are you sure? [Y/N]:接着问文件保存路径V7 里可能问得不太一样。脚本用exp_continue把两种情况都覆盖了——看到确认就按y看到输入文件名就直接回车用默认的flash:/startup.cfg直到回到提示符。最后是抓取配置。display current-configuration是华三的标准命令输出量大所以先关了分页。在发命令之后、log_file重新打开到正式输出文件这样文件里只记录配置正文登录横幅和命令回显都留在临时日志里后处理会省很多事。3.4 输出文件的后处理expect 抓下来的文件会带一些干扰内容比如命令回显、---- More ----残迹、控制字符。我在主控脚本里加一段清理cleanup_cfg() { local f$1 sed -i -e s/\r$// \ -e s/\x1b\[[0-9;?]*[a-zA-Z]//g \ -e /^---- More ----$/d \ -e /^display current-configuration$/d \ $f }这段命令的作用去掉 Windows 风格的回车符\r否则 Git 提交时每行都会显示^M去掉 ANSI 控制字符设备输出的颜色转义序列删掉翻页残留和命令回显行。清理完的文件才适合直接 diff 和入库。4. 实测中踩过的坑每一条都能让脚本跑崩写脚本花了一个小时调通前前后后折腾了大半天。下面这几个坑是按真实发生频率排序的每一个都让我“受益匪浅”。4.1 备份文件只有半截提示符结束标志匹配错了第一次跑出来的备份文件只有 600 字节打开一看内容到# sysname那一段就没了。原因就是我前面提到的——匹配结束标志时用了裸的#而华三配置的分区注释标志正好是#expect 看到第一段注释就认为命令输出完了提前收工。这个问题的排查路径是先手工登录设备确认display current-configuration输出正常再跑一次脚本打开临时日志发现配置输出还没到一半就停了最后定位到 expect 的结束标志被配置正文的#触发。解决方式就是把结束标志改成“行首出现的提示符”也就是(?n)^[\\[].*[\\]]这个正则。改完后备份文件大小从 600 字节变成 56KB和手工导出完全一致。这个坑看着不起眼但如果你把 expect 脚本抄回去用在别家设备上第一件事就是确认结束标志不会和配置正文撞车。4.2 卡死在 More 分页一条命令解决的问题华三设备输出内容超过一屏时会显示---- More ----等你按空格或回车翻页。如果这个状态不被处理expect 脚本会一直等在那里直到 timeout 报错。解决办法就是登录后第一时间执行screen-length disable相当于告诉设备“输出内容不要分页一口气发完”。这个命令必须在用户视图下执行也就是看到设备名提示符后马上发。但这里还有一个细节有些设备版本在screen-length disable之后如果后续命令输出特别长仍然可能出现---- More ----。所以我在display current-configuration的 expect 分支里保留了-re ---- More ---- { send ; exp_continue }双保险。这种“能设默认值就设默认值同时保留兜底处理”的思路在处理各种设备版本差异时特别管用。4.3 配置文件里全是 ^M 和乱码控制字符清洗第一次尝试用 Git 管理备份时git diff出来的每行末尾都带^M整个 diff 一塌糊涂。原因是设备 CLI 输出默认用\r\n换行expect 原样抓下来后文件里充满了回车符。另外部分华三设备开启终端类型协商后会在输出里夹带 ANSI 控制字符比如\x1b[42D这种光标移动序列肉眼看不见但用cat -A一看就知道有多乱。我的清理方案就是前面那段sed核心思想是“回车符、控制字符、翻页提示、命令回显”四类垃圾逐行清除。每次备份完建议抽查一两个文件用file命令看格式用cat -A看有没有残留控制字符。养成这个习惯后备份文件的质量会稳定很多。4.4 老设备的 SSH 算法不兼容连不上才想起这事有一批用了七八年的华三旧型号SSH 登录时直接报错提示no matching key exchange method found或no matching host key type found。原因是新版 OpenSSH 出于安全考虑默认禁用了旧的 KEX 算法和ssh-rsa主机密钥而这些老设备出厂固件只支持旧算法。正常的解决方式是给 ssh 命令显式指定兼容算法ssh -o KexAlgorithmsdiffie-hellman-group14-sha1 \ -o HostKeyAlgorithmsssh-rsa \ -o PubkeyAcceptedKeyTypesssh-rsa \ $user$ip把这个参数加到 expect 的 spawn 行里老设备就能连上了。严格的安全团队可能会认为弱算法有风险但应对老设备的办法不是“不备份”而是“备份时用弱算法、平时关掉设备的外网暴露面”。如果公司有合规要求建议和网络组商量老设备的升级计划而不是一直在兼容性上打补丁。4.5 save 命令把流程带跑偏不同版本确认方式有差异最早我的 save 部分写得很简单就发一个save force。后来发现 V5 的老设备根本不认force参数而是走交互式确认。如果不处理expect 脚本会一直卡在Are you sure? [Y/N]:等到超时然后继续往下执行——最终备份出来的配置和实际运行配置不一致因为 save 没成功。后来我把 save 逻辑改成“全兼容模式”发save\r后匹配两种情况一种是y/n确认一种是输入文件名用exp_continue持续处理直到回到提示符。这个兼容层看着啰嗦但实实在在省了后续很多麻烦。类似的还有save之后如果设备提示Please input the file name直接发一个回车用默认文件名不要在交互上纠结。5. 让备份自动跑起来crontab、增量保留与配置版本化管理脚本能手动跑通只是第一步真正让备份机制发挥价值的是自动化。我的做法分四层从低到高依次推进。5.1 用 crontab 定时执行在 Linux 上配置定时任务一行命令的事# 每天凌晨 2 点执行完整备份 0 2 * * * cd /opt/h3c-backup ./backup_h3c.sh devices.txt /data/backup /data/backup/cron.log 21crontab 环境里有个常见坑expect可能不在 PATH 里主控脚本里我用的是/usr/bin/expect就是防止 crontab 环境变量不完整导致找不到命令。另外建议在脚本开头加上#!/bin/bash和set -u避免未定义变量在定时任务里静默出错。5.2 备份文件保留策略备份文件按“IP_时间戳”命名时间一长肯定会堆积。我用 find 按天数清理# 保留最近 90 天的备份 find /data/backup -name *.cfg -type f -mtime 90 -delete放到 crontab 里每周执行一次。保留策略看你实际情况90 天算是个比较稳妥的默认值如果设备经常有变更可以改成 180 天。这里的关键是清理之前先确认 Git 仓库已经推到了远端否则本地文件删了历史也就真没了。5.3 把备份目录挂进 Git配置变更一目了然文件落盘只是“存下来”真正能发挥价值的是“可对比”。我把备份目录初始化成一个 Git 仓库每次备份完成后自动提交cd /data/backup git add -A git commit -m backup $(date %F_%T)这几行加到主控脚本末尾配合 crontab 就能形成“每天备份一次、每次备份一个提交”的历史线。以后任何一次配置被人改过git diff就能精确到哪一行、改了哪个字段。这个能力比备份本身还值钱。5.4 失败自动告警备份跑了不代表备份成功了。我的脚本里已经收集了SUCCESS和FAIL的统计数可以在结束阶段按需接入告警——最轻量的是用 curl 调企业微信或钉钉的机器人 webhook把失败设备和原因推送到群里。这里不贴具体的 webhook 代码了因为各家平台 API 一直在变思路就是“脚本执行完把结果发到一个统一入口人不用每天去看日志”。我更想强调的是告警机制的前提是“结果可信”。如果脚本本身的结束标志匹配不准、文件清理不干净告警再多也是噪音。所以我的建议是把前几章的踩坑逻辑都验证一遍确认备份文件能完整还原配置后再上自动化。6. 说说我自己现在的使用习惯这个脚本我用了快一年跑过三次大规模网络改造日常也靠 crontab 守着几十台设备。最大的感受是它省下来的不只是导出配置的那点时间而是“终于敢在晚上改配置了”的那点底气——改之前先备份改完再看一眼 diff心里有底得多。如果你要抄这套脚本我的建议是先拿三台不同型号的华三设备跑通注意观察临时日志里的交互过程确认save确认方式和display current-configuration结束标志在你的设备上是正常识别的再加设备、加 crontab、加 Git 提交。每个网络环境都有点自己的小脾气脚本最大的价值不是替你思考而是把重复劳动压缩到一次回车剩下的判断还是得你自己来。