去年给一家工厂做网络整改,现场六十多台华三交换机,光是把每台设备的配置导出来归档就花了我整整两天。真到了“改错一条策略想回退”的时候,你才发现自己手里根本没一份可靠的配置基线。后来我花了一个晚上,写了这套批量备份华三交换机的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 和 Ansible
2.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,H3C@123456 192.168.10.2,admin,H3C@123456 192.168.10.10,netadmin,Passw0rd#2024这里有两个约定。第一,密码里不要包含逗号,因为脚本用逗号做分隔符,密码里出现逗号会把字段拆乱;如果你的密码确实包含特殊字符,可以把分隔符整体换成|,对应把主控脚本里的IFS=','改成IFS='|'。第二,设备名也可以加进来,比如192.168.10.1,SW-CORE-01,admin,H3C@123456,这样输出文件名能直接体现设备角色,后面我会说怎么扩展。
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" SUCCESS=0 FAIL=0 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" 2>&1; 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 StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 $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这段脚本里有几个地方不是随便写的,逐个说清楚。
先说登录部分。StrictHostKeyChecking=no和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 KexAlgorithms=+diffie-hellman-group14-sha1 \ -o HostKeyAlgorithms=+ssh-rsa \ -o PubkeyAcceptedKeyTypes=+ssh-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 2>&1crontab 环境里有个常见坑: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 提交。每个网络环境都有点自己的小脾气,脚本最大的价值不是替你思考,而是把重复劳动压缩到一次回车,剩下的判断还是得你自己来。