深夜两点十七分,机房值班电话把我从床上拽起来——存储侧链路告警,数据库主机的多路径状态全部变成Degraded。等我远程登录光纤交换机,一条一条核对zone配置时,天都快亮了。事后定位,问题出在一个zone里多出来的一个成员WWN,把本来该隔离的两套环境串在了一起。这种靠人工翻配置排障的方式,真的很要命。所以我花了些时间,做了一个华为光纤存储交换机zone配置文件输出脚本1.0:定时登录交换机,把zone、alias、cfg相关的配置完整导出成TXT、JSON和CSV三种格式,形成可追踪、可对比、可恢复的配置基线。这篇博文就围绕这个脚本,从需求背景、配置模型、实现思路到踩坑记录,完整拆一遍。
1. 为什么我一定要做zone配置导出这件事
1.1 一次zone配置事故的启示
那次事故的过程其实很简单:变更窗口内有人想把新接入的存储端口加进某个zone,结果在Web界面上选错了zone名称,顺手保存并激活了配置。表面上看操作都成功了,但实际生效的zone里多了一个不该出现的成员,导致两个本应隔离的环境在Fabric层面能够互相看到对方的存储设备。主机侧的表现是某些LUN突然可见,另一些LUN开始报错,多路径来回切换,应用性能直线下降。
最痛苦的是定位环节。由于没有任何配置基线,我只能登录一台交换机,先跑cfgshow看有效配置,再逐个zoneshow核对zone成员,还要对照一份几个月前的WWN清单去猜“这个WWN应该属于谁”。三台交换机、两个机房,来回折腾了三个多小时才锁定问题。第二天复盘时团队达成了一个共识:zone配置不光要保证“当前正确”,更要保证“变更可对比”。如果没有一份每天自动生成的配置快照,下次出问题照样要靠人肉翻配置。
1.2 人工导出配置的三大痛点
在那之后我调研了一圈,发现大多数存储运维团队在做zone配置备份时,用的还是原始办法,痛点非常集中。
第一是格式不统一。有人直接复制cfgshow全屏输出到文本里,有人在Web界面一页页截屏,还有人只记录了effective config,defined config里的alias和zone全丢。等到真要恢复配置时,才发现手里的资料根本拼不出一份完整的配置。
第二是难以对比。文档里也没有统一的对比工具,只能用文本编辑器肉眼找差异。但zone成员顺序调整、行尾空格、换行符差异都会干扰判断。很多时候对比了半天,得到的结果依然是“看起来差不多,但又说不上哪里不同”。
第三是没有时间线。没人记录“这份配置是什么时候导出的”“变更前长什么样”。出了事只能靠记忆复盘,而记忆在凌晨三点通常是靠不住的。如果有一套自动导出机制,把每次导出都按时间戳归档,配置回溯就会变成一件很简单的事。
1.3 1.0版本明确做的事情和不做的事情
所以这个1.0版本脚本的定位非常克制:只做配置输出,不做任何变更操作。
它会做的事情包括:连接交换机、执行一组只读查询命令、采集交换机基本信息和zone相关配置、把原始文本和结构化数据分别落盘、按交换机名和时间戳归档。输出内容覆盖switchshow摘要、cfgshow完整原始文本、zone与alias成员映射表,以及一份JSON格式的结构化配置。
它明确不做什么:不自动修改zone、不批量下发配置、不做变更对比和告警、不做Web展示。这些能力留给后续版本。先稳住“把配置看清楚”这件事,是我给这个脚本定的第一原则。对于存储运维、网络运维、容灾演练和审计相关同学来说,这个脚本可以直接当你日常巡检和故障排查的辅助工具。
2. 动手之前,先把华为光纤交换机的配置模型捋清楚
2.1 zone、zone alias、zone config和effective cfg的关系
写脚本之前,如果对配置模型理解不到位,解析逻辑就会写歪。我用一个门禁系统的类比来解释。
一台光纤交换机就像一栋大楼的门禁系统。每个主机HBA和存储控制器端口都有自己的WWN,相当于每个人的门禁卡。zone alias是通讯录分组,比如把两台数据库主机的HBA聚合成一个DB_SVR组,方便统一引用。zone就是一条门禁规则,定义了“哪些门禁卡可以互相访问”,比如一个zone里放进DB_SVR和STO_ARRAY,数据库主机和存储阵列之间就能通信。zone config是一组zone的集合,相当于一套完整的访问控制方案。而effective configuration,是当前真正激活生效的那套方案。
重点在于:只有被加入config且执行了激活操作的zone才会真正影响流量,defined config里躺着的那一堆zone配置,是不会对数据面产生任何作用的。所以脚本导出时,不仅要记录所有zone的定义,还要清晰标注当前effective cfg里包含哪些zone。我在JSON结构里专门加了一个effective_cfg字段,目的就是让运维同学一眼看出“现在到底哪套规则在生效”。
2.2 华为SNS系列的命令行风格:FOS体系,不是VRP体系
这是很多第一次接触华为光纤交换机的人最容易踩的坑。
如果你以前管理华为的交换机、路由器,习惯性输入system-view想进配置模式,在这台存储交换机上会直接收到命令不存在的提示。存储网络环境里常见的华为SNS系列光纤交换机,命令行体系沿用的是Fabric OS风格,命令是cfgshow、zoneshow、aliashow、switchshow、cfgsave、cfgenable这一套,和运营商网络设备那套VRP命令完全是两回事。
这个差异对脚本的影响是决定性的。如果你按VRP思维去设计命令集和解析器,连接之后第一步就会卡住。所以动手写脚本前,一定要先确认目标设备的命令行风格。判断方法很简单:登录设备后敲一个version,如果返回的是Fabric OS相关的版本信息,就说明这是FOS体系,脚本可以按本文的思路来做;如果敲system-view能进去,那就是另一套命令体系,所有命令和解析逻辑都得重新设计。
2.3 账号权限规划:给脚本准备一个最低权限的只读账号
脚本要干活,就得先在交换机上有一个账号。但千万别图省事直接拿admin账号去跑定时任务。
FOS体系里有不同的账号权限层级,admin拥有全部权限,而导出配置只是查询和读取,完全不需要那么高的权限。我给脚本申请了一个独立账号,只用于登录和执行查询类命令,不授予任何写权限。这样做有几个好处:万一脚本本身有bug,也不会误改交换机配置;账号操作记录和运维人员的账号分开,审计时能明确区分哪些操作来自自动化任务;就算这个账号的凭据泄露,影响面也被限制在“能看配置”而不是“能改配置”。
另外建议优先使用SSH key认证而不是口令密码。定时任务跑起来之后,密码过期是个非常头疼的问题,而密钥认证可以避免这个麻烦。后面第4章会给出相关配置示例。
3. 脚本整体设计:三种输出格式与目录结构
3.1 为什么同时输出TXT、JSON和CSV
设计输出格式时,我一开始只想存一份cfgshow的原始文本。后来在实际使用中发现,不同角色对配置数据的需求完全不一样,单一格式根本不够用。
TXT格式是给人看的。它保留交换机型号、WWN、Domain ID、端口状态以及cfgshow的完整原始输出。出故障时,工程师可以像翻手册一样查看这份文本,甚至在极端情况下照着它手工重建zone配置。
JSON格式是给程序用的。原始文本虽然忠实,但要写代码做配置对比、查“某个WWN在哪些zone里”、或者把配置导入数据库,还是结构化数据最方便。JSON设计成嵌套结构,把alias展开后的成员、zone引用关系、cfg归属都梳理清楚。
CSV格式是给表格派同事用的。一张扁平的zone_members.csv,每行一个对应关系,包含zone名、alias名、成员WWN和设备备注。领导问“某个存储端口在多少个zone里”,拉进表格里筛一下就有答案了。
三种格式共用同一个解析结果生成,不存在各说各话的问题。
3.2 输出目录与文件命名规则
文件命名直接影响后续排查效率。我采用的规则是:交换机主机名_UTC时间戳.类型。
使用UTC时间而不是本地时间,是为了避免不同机房、不同时区的服务器生成的文件名混乱。目录结构如下:
backup/ ├── SNS_sw01/ │ ├── SNS_sw01_2025-01-15T030000Z.cfg.txt │ ├── SNS_sw01_2025-01-15T030000Z.cfg.json │ ├── SNS_sw01_2025-01-15T030000Z.zone_members.csv │ └── latest -> SNS_sw01_2025-01-15T030000Z.cfg.txt ├── SNS_sw02/ │ └── ... └── logs/ ├── run_2025-01-15.log └── error_2025-01-15.loglatest是一个软链接,指向最新一份配置,方便人工查看时快速定位。日志目录单独存放,不跟配置混在一起,避免脚本运行错误信息污染配置归档。
3.3 内部模块划分
脚本虽然叫1.0,但我还是按长期维护的标准来组织代码,拆成五个模块:
connect:负责建立SSH连接、认证、断开,以及连接参数管理。execute:负责在交换机上执行命令、收集输出、处理分页。parse:负责把交换机返回的文本解析成内部结构化数据。render:负责把结构化数据渲染成TXT、JSON、CSV三种格式。report:负责生成执行状态日志、错误信息和告警摘要。
这样拆的好处是,以后如果想把连接方式从SSH换成厂商提供的API接口,只需要改connect模块;如果想把输出格式增加一个YAML,只需要在render模块加一个函数。解析逻辑和展示逻辑分离,维护成本低很多。
4. 脚本核心实现:SSH抓取、文本解析与异常兜底
4.1 连接方式选择:netmiko还是paramiko
连接交换机,我选了netmiko而不是直接裸用paramiko。原因很简单:netmiko把很多网络设备的交互细节封装好了,尤其是分页处理、命令结束符判断这类问题,直接省掉大量容易出错的边界逻辑。
针对FOS体系,netmiko有对应的设备类型,连接配置大概长这样:
from netmiko import ConnectHandler device = { "device_type": "brocade_fos", "host": "192.0.2.10", "username": "zone_backup", "use_keys": True, "key_file": "/home/ops/.ssh/zone_backup_ed25519", "conn_timeout": 20, } conn = ConnectHandler(**device) cfg_output = conn.send_command("cfgshow") switch_output = conn.send_command("switchshow") conn.disconnect()有一点要提醒:netmiko对FOS体系的适配在不同版本里表现不太一样,建议先在测试环境里跑一条send_command("version")确认返回正常。如果遇到奇怪的分页截断问题,不要犹豫,直接降级用paramiko自己处理协议。灵活切换才是正经做法。
4.2 需要抓取的配置命令清单
整个脚本采集的命令其实不多,但每一条都有明确目的。
| 命令 | 作用 | 解析后得到的信息 |
|---|---|---|
switchshow | 查看交换机基本信息和端口状态 | 机型、WWN、Domain ID、端口与登录设备映射 |
cfgshow | 查看alias、zone、cfg定义及有效配置 | alias成员、zone成员、cfg归属、effective cfg |
zoneshow | 查看zone与成员的对应关系 | zone成员的具体WWN或alias名 |
aliashow | 查看alias与成员的对应关系 | alias成员的具体WWN |
version | 查看版本信息 | FOS版本号、机型信息 |
switchshow的输出对排障特别有价值,因为它能直接看到每个端口连接了哪台设备的WWN,这对把WWN翻译成人话至关重要。我在解析后会把端口信息和zone成员做一个交叉引用,输出到CSV里作为备注列。
4.3 从cfgshow文本到结构化JSON的解析
解析是所有环节里最需要耐心的一步。cfgshow的输出大致会分ali:、zone:、cfg:几个段落,每段内部有缩进和分号分隔的成员列表。解析器要做的事情是识别段落边界、提取名称、拆分成员,并处理alias引用。
一个简化的解析框架大概长这样:
import re def parse_cfgshow(raw): result = {"aliases": {}, "zones": {}, "cfgs": {}} section = None current_name = None for line in raw.splitlines(): striped = line.strip() if re.match(r"^(ali|zone|cfg):", striped): section = striped.rstrip(":").strip() continue if not striped or striped.startswith("-"): current_name = None continue if striped.endswith(":"): current_name = striped.rstrip(":").strip() result[section + "es"][current_name] = [] continue if current_name and section in ("ali", "zone", "cfg"): members = [item.strip() for item in striped.split(";") if item.strip()] result[section + "es"][current_name].extend(members) return result实际上真实的cfgshow输出比这个示例复杂得多,会有嵌套、注释、续行等情况。所以我强烈建议:写解析逻辑之前,先用命令把每台交换机的真实输出保存为样例文件,对着样例逐行调试。千万别凭想象构造输出格式,那样解析器一定会在第一台真实设备上翻车。
另外要特别注意alias引用。zone的成员列表里可能出现的是alias名而不是具体WWN,解析时需要把alias展开成实际WWN,才能在CSV里呈现出完整的对应关系。
4.4 异常处理清单
脚本跑在无人值守的定时任务里,异常处理比功能本身更重要。我整理了下面这套兜底策略:
- 连接超时:重试3次,每次等待时间按5秒、10秒、20秒递增,避免反复连一台故障设备拖垮整个任务。
- 命令执行失败:把原始回显完整保存到
debug/目录,方便事后人工排查,同时标记该交换机本次导出失败。 - 交换机不可达:写进日志和报告,不中断后续交换机的处理。
- 返回内容为空:先重试一次,仍然为空则告警,防止抓到了半截空配置还当成正常结果。
- 磁盘空间不足:写文件前检查剩余空间,空间不够时直接跳过归档并告警。
这一层做扎实了,定时任务才敢放心让它每天跑。毕竟一个“凌晨三点告诉你哪台交换机没抓到配置”的报告,比“第二天才发现昨天的配置文件是空的”要友好得多。
5. 实测中那些文档不会告诉你的坑
5.1 分页输出导致配置被截断
第一次跑定时任务,第二天起来看日志一切正常,但打开JSON文件发现后半段zone配置不见了。排查了很久才发现,问题出在分页。
FOS在执行长输出命令时默认会分页,交互式终端里会显示--More--等待用户翻页。我的脚本通过SSH通道执行命令时,如果没有正确处理分页提示,输出会在某一页就停下来,后面的配置全被丢弃。
解决思路有两种。一种是在连接初始化时执行分页关闭命令,我实际用的是发送pager -1把分页功能关闭;另一种是用非交互方式执行命令,例如直接通过SSH执行单条命令而不是进入交互式shell。netmiko的brocade_fos设备类型通常已经处理了分页,但如果你发现输出异常中断,优先检查是不是这个问题。
这个坑最大的隐蔽性在于:脚本本身没有报错,配置看起来也“正常”,只是少了一段。所以我在脚本里加了一个自检逻辑——解析完成后统计zone数量,如果跟上次导出的数量偏差超过阈值,就额外输出一条提示。
5.2 zone名称里的特殊字符
规范建议里,zone和alias名称应该只用字母、数字和下划线,但现实中的老系统根本不管你。我遇到过带中划线的、带中文的、甚至里面有空格的在zone名称。
如果解析器用简单的split()切割,遇到空格就会把名称拆碎,导致解析结果错位。处理办法是:切分成员列表时明确按分号处理,成员两端的空格和不可见字符统一用strip()清理;名称行的识别不能依赖固定位置,而是以冒号结尾作为当前条目名称的标志。简单说,所有逻辑都要以“真实输出是什么样”为准,而不是以“文档里说应该是什么样”为准。
5.3 Active和Standby两台交换机配置不一致
很多环境里交换机是成对部署的,配置理论上应该一致,但实际操作中经常出现偏差。比如某次紧急变更只做了单台,又或者变更过程中有一台设备重启回退了配置,都会导致主备两台设备的zone配置出现差异。
脚本必须把每台交换机当成独立个体分别导出、分别归档,绝不能图省事只导一台然后复制成两份。导出完成后,我会把两台设备的JSON做一次深度对比,输出差异摘要。这个对比逻辑虽然简单,但每次做容灾演练前跑一遍都特别有用,能提前发现很多潜在问题。
5.4 SSH host key变更让脚本突然失败
交换机固件升级、硬件更换,甚至某些恢复出厂设置的操作,都会改变SSH host key。脚本如果用严格校验模式,会在这些操作之后直接报host key不匹配,拒绝连接。
我的处理方式是使用paramiko的自动接受策略,同时把每次连接的主机指纹记录到日志文件里:
import paramiko client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname="192.0.2.10", username="zone_backup", key_filename="/home/ops/.ssh/zone_backup_ed25519")对内网管理网段、独立低权限账号的自动化任务来说,自动接受新指纹是一个可接受的取舍。如果你对安全要求更高,就把交换机的指纹固化在配置文件里,发生变更时人工确认后再更新。
6. 1.0版的边界和我下一步的打算
6.1 明确不支持的场景
1.0版脚本不是一个管理平台,它的能力边界很清楚:只适配FOS命令行的华为SNS系列光纤交换机,不覆盖其他命令行体系的华为网络设备。它只做只读导出,不做任何配置变更。它不解析私有加密字段,也不处理Web界面里保存但未同步到命令行的配置状态。
这些限制写进README里,反而让脚本用起来更踏实。使用的人不会对一个“导出工具”产生不切实际的期待,也就不会在凌晨两点发现它不能帮你改zone时骂娘。
6.2 从“导出”到“对比”:配置漂移检测
1.0跑稳定之后,我下一个计划是加配置漂移检测。思路很简单:每天导出后,把当天JSON和前一天JSON做结构化对比,生成变更报告,标出哪个alias加了成员、哪个zone被删除、哪个cfg的激活状态发生变化。
实现上,先把JSON里的嵌套结构按固定顺序序列化,再做逐字段比较,这样能避免“只是成员顺序变了”导致的误报。变更报告按天归档,形成一条完整的配置变更时间线。如果再进一步,可以把报告接入现有告警通道,让配置变更在发生当天就通知到人,而不是等出故障了才被动排查。
6.3 给想写类似脚本的人的三条建议
第一,先花半小时手工登录交换机,把真实输出保存下来,对着样例写解析器。解析逻辑写得再漂亮,匹配不上真实输出也是废的。
第二,配置历史不要用裸文件堆着。可以用git管理,每次导出提交一次,天然就有差异对比能力;也可以导入数据库,方便按WWN或zone名检索。
第三,权限最小化、账号独立、跑在受控的管理机上。脚本本身不是核心,围绕脚本的运维规范才是保障长期可靠的关键。先在一台交换机上跑通整个流程,再逐步扩展到其他设备,这个节奏比较稳妥。
最后说一点个人体会。运维的底层能力,很大程度取决于你手上有没有一份能随时翻出来对比的配置基线。这个1.0版本虽然功能朴素,但在我后续的几次变更和一次故障复盘中,都让我心里有底。如果你也在维护多台光纤交换机,建议从最核心的那台开始,让它每天自动留下一份配置快照。一个月后回头看,你会觉得当初花这个把小时写脚本,很值。