资讯动态

Python telnetlib实现网络设备自动化登录与配置备份实战

发布时间:2026/10/24 1:06:31 来源:尧图企业网站定制
1. 为什么网络工程师都绕不开这个telnet实验这两年网络自动化转型喊得响但很多在机房摸爬滚打的工程师觉得Python离自己还远天天喊着学自动化结果回到工位上还是老老实实敲命令行。我见过不少同行手里管着几十台核心设备每次加个VLAN、查个接口状态都得挨个登录、逐条敲命令不仅慢还容易出错。等真正把Python捡起来学着写第一个脚本去登录设备时才意识到这条路一旦走通效率是成倍的提升。而telnet远程登录设备实验就是这条路上最基础、也是最关键的一个门槛。为什么说telnet原因很简单虽然现在生产环境里telnet因为明文传输已经逐渐被SSH替代但模拟器、老旧设备、内网实验环境里telnet依旧大量存在而且telnet协议本身足够简单非常适合用来理解“程序如何与网络设备交互”这件事。你先把telnet跑通了再去看SSH、NETCONF、RESTCONF会发现底层思路是共通的只是加密和数据结构变得更复杂而已。我见过不少新手一上来就死磕Paramiko和Netmiko结果SSH的密钥交换、主机密钥校验、算法协商这些概念一大堆半天连不上设备很容易劝退。反过来先用Python自带的telnetlib库把“登录-敲命令-看结果-退出”这个闭环跑通你会对整个自动化流程建立最直观的体感后面切到更高级的库就是水到渠成的事。这篇内容不是教科书式地贴一段代码就完事我更想把实验里每一步的“为什么”讲清楚包括选型考量、环境搭建、代码实现的每个细节、我在真实环境里踩过的坑以及一套从“能跑”到“能用”的完整优化思路。适合刚接触网络自动化的运维工程师、在校学生以及那些已经把Python语法过了一遍但不知道怎么和网络设备结合起来的人。哪怕你现在手头没有任何真实设备我也会介绍怎么用模拟器搭一个完全本地化的实验环境零成本复现整个流程。2. 实验方案选型为什么是telnetlib实验环境怎么搭2.1 技术选型的核心逻辑在动手写代码之前先把技术选型的逻辑理清楚。Python操作网络设备常见的有这么几条路线直接用socket写TCP协议、用telnetlib、用Paramiko走SSH、用Netmiko封装好的库、再往上还有Nornir这种批处理框架。选择telnetlib作为切入点并不是因为它功能最强恰恰相反它在几个方案里是最“原始”的但原始恰恰是优点。telnetlib是Python标准库的一部分不需要额外安装任何第三方包你在任何一台装了Python的机器上都能直接运行。它实现了telnet协议的客户端底层就是TCP连接加一些协商命令的处理。相比直接用socket写telnetlib帮你处理了IAC命令协商比如终端类型、窗口大小这些省掉了很多麻烦而相比Paramiko和Netmikotelnetlib没有复杂的加密握手逻辑也没有各种高层次的设备适配抽象层你看到的就是最本质的“发字符串、收字符串”。很多教程一上来就推Netmiko说它支持几十种设备平台、自动处理提示符用起来多方便。这个观点没错但我建议初学者不要从Netmiko起步原因在于Netmiko把太多东西封装掉了你输入一条命令它帮你登录、等提示符、发命令、再等提示符、收集输出每一步内部发生了什么新手完全无感知。一旦设备返回的格式和预期不符或者登录卡住你根本不知道去哪里排查。而telnetlib逼着你自己处理“发送”“接收”“等待”“判断”这些基本动作等你把这些底层逻辑吃透了再用Netmiko就是降维打击。2.2 实验环境的三种搭建方式这里我给大家梳理一下实验环境的搭建路径三种方式各自适用不同阶段。第一种物理真机。公司机房如果有闲置的交换机或路由器直接连console线或用管理IP进行telnet登录这是最真实的环境。但真机实验有两个问题一是如果误操作比如ACL写错把自己拒之门外可能影响生产业务二是很多人家里根本没有设备。所以真机适合有经验的人做验证不适合新手折腾。第二种GNS3或EVE-NG这类模拟器。它们里面跑的是真实的网络设备镜像功能和真机几乎一致支持VLAN、路由协议、ACL等高级特性telnet服务也默认开启。EVE-NG甚至可以直接模拟一组完整的企业网络拓扑。这种方式最适合做接近生产环境的实验唯一门槛是需要找到对应的设备镜像。第三种也是我最推荐新手起步的是Cisco Packet Tracer这类轻量模拟器。它虽然功能不如GNS3完整但胜在安装简单、设备启动快、自带图形化拓扑telnet服务默认开启做远程登录实验完全够用。我用Packet Tracer搭过一个简单的拓扑两台交换机一台配好管理IP和telnet密码另一台作为终端设备然后让Python脚本去登录第一台交换机执行查询命令。无论选哪种方式实验的目标是一致的准备一台开启了telnet服务、配置了登录认证的网络设备确保你的电脑能够通过telnet协议访问到它然后让Python脚本代替手工交互完成登录和命令执行。2.3 设备端的最小配置清单这一步非常关键。很多人脚本本身写得没问题但设备端基础配置没做好导致通关失败。在网络设备上做telnet实验最少需要确认四件事第一接口要有IP地址且能路由可达。交换机的管理VLAN接口需要配上IP比如VLAN 1的接口地址是192.168.1.254/24你的电脑是192.168.1.100/24两者要能ping通。第二telnet服务要打开。不同厂商命令不同思科系的交换机和路由器默认VTY线路配置里包含transport input telnet或transport input all有些老设备默认允许telnet有些新版本默认只允许ssh需要手工修改。华为系的设备则需要在VTY接口视图下配置protocol inbound telnet。第三要设置登录认证。VTY线路下配置password并且login同时需要配置enable密码或全局权限密码否则登录进去只能处于用户模式什么查询命令都执行不了。第四域名解析要关闭。这个细节我后面会专门讲很多人在模拟器上遇到的“登录卡死”问题罪魁祸首其实就是设备在做DNS反向解析。我用Packet Tracer实验时交换机的配置大概是这样的enable conf t hostname SW1 interface vlan 1 ip address 192.168.1.254 255.255.255.0 no shutdown exit line vty 0 4 password cisco123 login transport input telnet exit enable password cisco123 no ip domain-lookup做完这步你先在电脑的CMD窗口里手动telnet一下确认能否正常登录、执行命令。手动验证通过后再写Python脚本这样可以有效隔离问题到底是环境的问题还是脚本的问题一次就能判断清楚。3. telnetlib核心代码全解析从登录到执行命令3.1 第一个能跑的脚本我先贴一段最精简的代码这段代码完成了“连接设备-等待登录提示-输入密码-进入特权模式-执行命令-查看结果-退出”的全流程。建议你亲手敲一遍不要复制粘贴敲的过程中你才会注意到那些字符串细节。import telnetlib import time HOST 192.168.1.254 PORT 23 USERNAME # 有些设备VTY不要求用户名仅密码 PASSWORD cisco123 ENABLE_PASSWORD cisco123 tn telnetlib.Telnet(HOST, PORT, timeout10) tn.read_until(bPassword:, timeout5) tn.write(PASSWORD.encode(ascii) b\n) time.sleep(1) output tn.read_very_eager().decode(ascii, errorsignore) print(初次登录回显\n, output) tn.write(benable\n) time.sleep(1) tn.read_until(bPassword:, timeout5) tn.write(ENABLE_PASSWORD.encode(ascii) b\n) time.sleep(1) tn.write(bshow running-config\n) time.sleep(2) output tn.read_very_eager().decode(ascii, errorsignore) print(命令输出\n, output) tn.write(bexit\n) tn.close()这段脚本结构不复杂但里面有几个点新手极易踩坑我拆开来讲。3.2 为什么read_until和read_very_eager要搭配使用telnetlib最核心的交互模式是“读-写-读”。read_until方法的作用是阻塞读取直到从设备返回的数据中出现你指定的字节串或者超过设置的超时时间。比如read_until(bPassword:, timeout5)意思是程序在这里等着一旦收到包含Password:的字节流就立即返回最多等5秒。这里有个很关键的原则设备端每个交互环节的提示符必须和你代码里等待的字符串完全匹配。实际操作中我发现很多人在模拟器上遇到的第一个问题就是大小写和空格不匹配。比如设备提示Password:代码写成password:永远匹配不上直接等到超时。另外有些设备在密码提示符前面还有用户名提示符比如Username:如果你的设备开启了用户名认证就必须先等待并输入用户名再等待密码提示符。read_very_eager的作用则是读取当前缓冲区里的所有数据不等待。它通常在write发送命令之后配合time.sleep使用先sleep一小段时间给设备一个处理和回显的时间窗口然后一次性把这段时间内产生的输出全部读回来。为什么不直接再用read_until因为很多命令的输出是分批次到达的而且每台设备的回显时机不一样你未必清楚下一个提示符具体是什么。用“sleepread_very_eager”的方式虽然不够优雅但对初学者理解交互过程非常有帮助而且实验环境下完全够用。等后面你熟练了再去研究用read_until等待特定提示符的更精准做法。另外务必强调一个编码问题。telnetlib收发的是字节串不是字符串。PASSWORD.encode(ascii)就是要把你写的字符串转成字节如果你直接写tn.write(PASSWORD)会直接报错。设备返回的数据也是字节需要decode成字符串再打印否则输出的中文和符号全是乱码。3.3 密码输入和enable模式的细节设备厂商的登录流程差异很大。思科路由器交换机经典的登录流程是telnet连接建立后提示Password:直接输入VTY密码然后进入用户模式再输入enable命令和enable密码进入特权模式。华为设备则是先提示Username:再提示Password:登录后默认直接进入用户视图执行system-view进入系统视图。所以你写代码之前先手动telnet一次把这个交互顺序完整记录下来代码才能写得准。上面代码里有一个小细节值得注意我在输入密码之后加了一个time.sleep(1)。这是做什么的因为设备在接收密码后会进行认证处理然后回显一些信息比如版本信息、警告横幅等这些信息不是瞬间全部到达的需要一个小延时让数据完整进入缓冲区。如果你立刻执行read_very_eager可能会只读到半个屏幕的回显甚至在极端情况下读到空内容。enable命令的处理逻辑也是同理。思科设备输入enable后如果设置了enable密码会再次提示Password:注意这次的提示符可能和VTY登录时一模一样但含义不同。代码里用read_until等待Password:出现在技术上够用但如果你希望代码更健壮可以根据前面登录的状态和上下文来判断当前处于哪个环节。实验阶段可以简单统一处理但心里要有这个认知。3.4 为什么命令后面要加b\n很多刚接触这个实验的人会问命令后面为什么要加换行符因为telnet是基于行的交互设备端的命令行解析器并不是你每敲一个字符就立即执行而是等到收到回车键也就是换行符\n才把整条命令提交解释执行。所以你在write发送命令内容之后必须加b\n代表“命令输入完毕回车执行”。这个看似简单的点恰恰是很多自动化脚本线上事故的来源。有些人在Python的字符串里没加换行或者加的是别的符号设备端收到的就是一个不完整的命令字符串既不执行也不报错就卡在那里脚本后面的读取逻辑全部超时。还有一个相关的细节是密码后面也必须加换行。设备在读取密码时不会回显你输入的字符但回车确认这个动作是必须的。如果你的密码是cisco123发送的内容必须是cisco123\n而不是只有密码本身。4. 从“能登录”到“能干活”实验的完整实现4.1 让脚本适应不同设备的交互差异前面那版最小脚本在思科模拟器上能跑通但拿到华为设备上大概率要出问题因为交互流程不同。真实生产环境里你管理的设备往往是异构的可能有思科、华为、H3C、锐捷等多种品牌混在一起。一个合格的自动化脚本必须能适配这种差异。我自己的做法是先把不同厂商的交互特征抽象出来。比如有的设备需要输入用户名有的不需要有的登录后直接是特权模式有的需要再执行enable有的命令提示符是“ ”有的是“Router”。我倾向于写一个简单的设备配置字典把每种设备的关键特征标识存储起来device_type_config { huawei: { need_username: True, login_prompt: bUsername:, password_prompt: bPassword:, enter_enable_cmd: None, privilege_prompt: b, enable_password_prompt: bPassword:, terminal_prompt: b], # 系统视图提示符 }, cisco: { need_username: False, login_prompt: bPassword:, password_prompt: bPassword:, enter_enable_cmd: benable, privilege_prompt: b#, enable_password_prompt: bPassword:, terminal_prompt: b#, } }然后写登录函数时根据这个配置轮询等待提示符。这里可以用一个技巧不固定只等待某一个提示符而是用read_until配合一个提示符列表哪个先出现就处理哪个。telnetlib没有原生支持多提示符等待但你可以通过循环read_very_eager加上判断来实现。我这里写一段完整的登录函数给你参考它能把登录过程收敛成一个可复用的工具import telnetlib import time def telnet_login(host, port, username, password, enable_password, need_username, login_prompt, password_prompt): tn telnetlib.Telnet(host, port, timeout15) output b # 等待并处理登录 if need_username: output tn.read_until(login_prompt, timeout10) tn.write(username.encode(ascii) b\n) output tn.read_until(password_prompt, timeout10) tn.write(password.encode(ascii) b\n) else: output tn.read_until(password_prompt, timeout10) tn.write(password.encode(ascii) b\n) # 有些设备登录后还会推送横幅信息这里给设备一点时间 time.sleep(1.5) output tn.read_very_eager() return tn这样做的好处是你的核心登录逻辑只需要写一次后面接什么厂商的设备改配置字典就行不用每个设备各写一套代码。生产环境中这种“数据与逻辑分离”的思想特别重要你维护的不是几十段重复代码而是一个配置表加一个统一入口。4.2 执行命令并稳健地收取完整回显登录完成只是第一步真正干活是执行命令。设备执行命令后回显多少内容、分几批到达都取决于命令本身和设备状态。查询类命令比如show running-config输出可能几百行保存配置命令可能只有一两行回显。收完整输出的标准做法不是sleep一个固定时间然后read_very_eager那样容易截断长输出。更可靠的方法是设置一个“沉默阈值”检测循环读取每次读取后重新计时如果在设定的时间内没有新的数据到达就认为设备输出完毕。这是一个很实用的技巧生产级别的自动化脚本几乎都这么干。我直接给出一个可用的实现def send_command(tn, command, wait_time2, timeout15): tn.write(command.encode(ascii) b\n) output b start time.time() last_data_time time.time() while True: if time.time() - start timeout: print(执行命令超时停止等待) break try: chunk tn.read_very_eager() except Exception as e: print(f读取异常: {e}) break if chunk: output chunk last_data_time time.time() else: if time.time() - last_data_time wait_time: # 沉默期达到阈值认为命令输出结束 break time.sleep(0.2) return output.decode(ascii, errorsignore)这个函数思路不复杂持续尝试读取非阻塞数据只要有数据来就刷新沉默计时器一旦连续wait_time秒没有新数据就判定当前命令的输出结束。超时兜底用总时长timeout控制防止某些命令比如进入一个交互式子菜单一直不返回。实测下来这个方法对大多数查询命令的收包效果都很好。当然它也不是银弹如果是ping这种可能持续输出几十秒的命令你需要把timeout调大或者把wait_time阈值调高这些参数按需调整即可。4.3 完整实验批量备份交换机配置我经常拿“批量备份配置”来验收实验成果因为它包含了登录、执行命令、收集结果、落盘存储这几个自动化必备环节真实工作中需求也很大。下面这段代码就是建立在send_command函数基础上的批量备份脚本框架。import os import time from datetime import datetime def backup_switch_config(host, username, password, enable_password, config, backup_dirbackup): os.makedirs(backup_dir, exist_okTrue) tn None try: tn telnet_login( hosthost, portconfig.get(port, 23), usernameusername, passwordpassword, enable_passwordenable_password, need_usernameconfig.get(need_username, True), login_promptconfig.get(login_prompt, bUsername:), password_promptconfig.get(password_prompt, bPassword:), ) # 进入特权模式 enable_cmd config.get(enter_enable_cmd) if enable_cmd: send_command(tn, enable_cmd, wait_time1) send_command(tn, enable_password, wait_time1) # 执行显示配置命令 show_cmd config.get(show_config_cmd, display current-configuration) config_text send_command(tn, show_cmd, wait_time3) # 保存到文件 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename os.path.join(backup_dir, f{host}_{timestamp}.txt) with open(filename, w, encodingutf-8) as f: f.write(config_text) print(f设备 {host} 配置备份完成: {filename}) send_command(tn, quit) except Exception as e: print(f设备 {host} 备份失败: {e}) finally: if tn: tn.close() device_configs { 192.168.1.1: { need_username: True, login_prompt: bUsername:, password_prompt: bPassword:, enter_enable_cmd: None, show_config_cmd: display current-configuration, }, 192.168.1.2: { need_username: False, login_prompt: bPassword:, password_prompt: bPassword:, enter_enable_cmd: enable, show_config_cmd: show running-config, }, } for host, config in device_configs.items(): backup_switch_config( hosthost, usernameadmin, passwordadmin123, enable_passwordenable123, configconfig, )这个脚本逻辑很直观遍历设备列表按各自的配置调用登录函数执行查询命令把结果按设备IP和时间戳存档。你在生产环境里做配置备份、巡检信息收集核心框架都是这个套路。需要扩展的话可以在命令行参数里传入不同的命令序列或者把结果再解析成结构化数据进行对比分析。我在自己的实验里试过把这段脚本丢给一台模拟的思科交换机跑整个流程大约5秒完成比手动登录执行命令快得多而且输出的备份文件用文本对比工具一拉配置变更一目了然。这就是自动化最直观的价值。4.4 参数选择和调优经验这里的超时参数选择很多教程都是一带而过但实际影响的体验差别很大。timeout设太小设备响应稍慢一点就报错中断设太大设备真的卡住了你要干等很久。结合我自己的经验给出一组比较稳妥的初始值连接阶段timeout给10到15秒这个阶段包括了TCP三次握手和设备弹出登录提示符的时间实验环境一般毫秒级返回但模拟器刚启动时可能很慢10秒以上是必要的。登录过程中等待提示符的timeout给5到10秒足够。命令执行的沉默等待时间wait_time查询类命令给2到3秒保存类命令给1秒就够。整体命令执行超时timeout建议根据命令复杂度调整比如show running-config这种大输出15到20秒是合理的。另外说一个很多人不知道的细节模拟器里的设备启动速度往往比真机慢很多。Packet Tracer启动初期设备CPU占用率高telnet响应可能很慢。所以如果你在模拟器上实验建议等设备完全启动、能正常ping通了再跑脚本不要连上去就立刻执行命令。5. 处理不同厂商设备的命令交互差异5.1 进入系统视图和退出机制的差异网络工程师的实验做到这里往往对telnet网络设备执行指令有基础体验了接下来绕不开的就是多厂商适配问题。前三章我们核心围绕思科模拟器展开了实验但真实运维场景里你大概率会同时面对思科和华为或者其他国产品牌。不同厂商的设备对telnet的支持程度、命令交互提示符、以及进去之后的权限模型都有差异拿单一厂商的脚本直接通吃是不现实的。就拿思科和华为来说。思科的交换机telnet进去默认是用户模式提示符是Switch你需要手动执行enable进入特权模式然后才能看配置、改配置。而华为的设备telnet进去默认是用户视图提示符是 在这个视图下能看基础配置但如果想改配置需要额外执行system-view进入系统视图提示符变成[Huawei]。如果你的脚本只针对思科写死在“enable”直接搬到华为设备上命令就会被识别成未知指令根本不会进入管理态。不同厂商的退出机制也不一样。思科是exit一层层退华为是quit返回上一层return直接回到用户视图。如果脚本统一用exit在华为设备上系统视图下exit,返回的是用户视图而不是完全断开telnet连接。这类差异造成的“假连接”状态很容易被忽视导致后续脚本逻辑错乱。我的做法是把设备交互特征做成配置表而不是到处写if else。前面的代码里已经展示了这个思路实际操作中你完全可以维护一份YAML或者JSON配置文件把每一类设备的登录提示符、登录后需要执行的提权命令、进入配置模式的命令、以及退出命令都记录在一个结构体里。主程序只写一遍设备差异全部交给数据去描述。5.2 屏蔽分页和交互式提问的常见坑另一个多厂商实验里必然遇到的坑是“分页显示”。思科设备看长配置时默认是分页的屏幕输出满一屏就会停下来提示--More--等待你按空格继续。如果你的脚本只发了show running-config然后傻等设备吐出完整配置大概率会卡住因为设备在等你的翻页输入。解决思路两种。一种是在发送命令之前先通过terminal length 0或者screen-length 0 temporary之类的命令关闭分页显示另一种是在脚本里循环检测--More--字符串遇到就补一个空格。第一种方法更干脆也是生产环境里大家普遍的做法。但要注意terminal length 0在部分平台是会话级配置只在当前telnet会话内生效脚本每次登录后执行一遍就好。交互式提问题更隐蔽。有些设备在保存配置时会问是否覆盖现有文件在删除配置时会问是否确认删除在SSH连接首次建立时会问是否接受主机密钥。这些交互式提问如果不在脚本里预判并处理命令发出后脚本就一直卡在等待状态直到超时。处理方式和分页类似要么在命令后面附加“yes/no”参数比如华为的save可以直接写save y;要么在代码里用read_until等待提问出现再自动应答。我之前做自动化巡检脚本时就在华为设备保存配置这一步卡了许久查了半天才发现设备弹了个确认保存的提示脚本一直在等着读数据白白等满了超时。5.3 提示符匹配的策略升级前面我们用read_until等待固定的Password:这种硬匹配方式在登录环节没问题因为登录提示符全网设备都差不多。但进入命令执行阶段后提示符会随着设备视图变化比如用户模式是Switch特权模式是Switch#全局配置模式是Switch(config)#。如果脚本要准确地判断命令执行完成就需要匹配到对应的特权模式提示符。但这里有个麻烦不同厂商的特权模式提示符不一样。思科是#华为是]系统视图或者用户视图H3C和华为类似。如果统一用“#”作为命令执行完成的标志那么在华为设备上你永远匹配不到这个符号。所以提示符匹配也必须是配置化的不能写死在代码里。我在实验里常用的一种升级做法是把设备配置里的terminal_prompt字段设计成正则表达式列表然后用telnetlib的expect方法替代read_until。expect方法可以不传固定字节串而是传一个正则表达式列表只要任意一个正则表达式匹配成功它就会返回对应的索引和匹配内容。这个方法非常适合跨厂商环境因为不同设备的提示符都可以用一个宽松的正则去兜底匹配。import re def wait_for_prompt(tn, patterns, timeout15): # patterns 是编译好的正则表达式列表 index, match, text tn.expect(patterns, timeouttimeout) return index, text实际使用中我会把思科的特权提示符配成[rb][#]华为的配成[r][]]这样既能匹配提示符又不会把配置内容中恰好出现的符号误判为提示符。不过expect也有它的局限性比如正则写得太宽泛会把设备回显的命令正文里包含类似字符的片段误认为提示符导致命令还没执行完就提前返回。所以配置pattern的时候最好还是尽量贴近实际提示符格式不要图省事搞一个通配#否则排查问题会让你怀疑人生。5.4 多厂商命令集和数据解析的适配如果实验内容进一步深入到“抓设备状态”你会发现不同厂家的命令措辞和输出格式差异更大。比如查看接口状态思科是show interfaces华为是display interface查看路由表思科是show ip route华为是display ip routing-table。如果你要做一个跨厂商的巡检脚本命令集本身就需要做成映射表不同设备类型拉到对应的命令执行。更麻烦的是输出格式解析。思科的接口状态输出用“up/down”表达华为用“UP/DOWN”;思科的MAC地址表用“VLAN/MAC Address/Type/Port”这种表头华为又是另一种排版。如果想把每个接口的状态提取成结构化数据做资产盘点单纯拿到全文输出只是第一步后面还需要根据不同的输出格式写不同的正则解析器。所以我的建议是新手做实验时不要贪多求全地搞一个万能脚本而是先从单一厂商的一个典型场景切入比如“批量查询思科交换机接口状态”。把这个场景的数据解析、异常处理做透之后再横向扩展到其他厂商把命令映射和解析器逐层抽出来慢慢你就会得到一套属于自己的网络自动化工具箱。我自己的很多模块就是这样一点点攒出来的刚开始只有一个telnet登录函数后来不断加命令、加解析、加报表输出才变成一个能应付多厂商的脚本集。6. 把脚本变成真正能落地的工具6.1 封装登录函数与核心工具函数在第一版脚本跑通之后我强烈建议你不要停留在“能跑”就行的状态而是立刻去做重构和封装。我就吃过这个亏早期写自动化脚本时每个实验复制粘贴一份登录代码后来设备类型一多同一段代码维护出好几个版本改一个密码策略要改十几个文件教训相当深刻。重构的第一步是抽出独立的函数。把telnet连接、登录流程、执行命令、关闭连接这四个动作分别写成函数放到一个公共模块里。后续你在任何新实验里需要用到telnet都直接import这个模块不用重写第二遍。这个模块结构大概长这样# net_telnet.py import telnetlib import time def connect(host, port23, timeout15): return telnetlib.Telnet(host, port, timeouttimeout) def login(tn, username, password, enable_password, device_config): # 根据 device_config 处理不同的登录提示符和提权流程 pass def send_command(tn, command, wait_time2, timeout15): # 实现稳定的命令发送和回显收集 pass def close(tn): tn.close()这个封装带来的直接好处是业务脚本和底层交互逻辑分离了。业务脚本只需要关心“我要连哪些设备”“要执行哪些命令”“结果存到哪里”不用再处理read_unti、read_very_eager、字节编码这些细节代码可读性提升一个档次。6.2 引入多线程批量处理设备真实运维场景里几十台设备是常态一台一台登录显然是低效的。Python处理这种问题的最直观方案是线程池。注意我说的是多线程而不是多进程因为telnet操作主要是I/O等待网络延迟、设备响应时间在整体耗时里占比最高多进程在这里并不会有明显收益反而让代码复杂度上升。线程池的引入非常轻量from concurrent.futures import ThreadPoolExecutor hosts [192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4] def process_host(host): # 连接设备、执行命令、保存结果的完整逻辑 print(f完成 {host} 的处理) # 最多同时处理5台设备 with ThreadPoolExecutor(max_workers5) as executor: executor.map(process_host, hosts)但多线程也会引入一个容易忽略的问题线程之间是并发执行的如果每个线程都要写同一个日志文件或者往同一个目录存配置文件就可能出现文件写入交错。解决方式是给文件名加上设备IP和时间戳让每个线程写各自的文件日志方面可以用threading.Lock来保证同时只有一个线程写日志。我这个顺序踩过坑才觉悟早期多线程脚本并发写同一个csv结果文件数据东漏一条西漏一条查了半天才发现是文件写入冲突。另外并发程度要根据设备承载能力调整。我曾经图快用10个线程同时登录一台低端交换机结果设备CPU跑到100%部分telnet连接直接超时。后来学乖了生产设备的并发数控制在3到5之间宁可慢一点也不要把设备搞宕机。6.3 日志记录和错误处理一个能落地的工具必须有完善的日志记录和错误处理机制。很多初学者只处理“正常路径”一旦设备密码错误、IP不可达、命令回显异常脚本直接就崩了连个像样的报错都没有。我用logging模块给脚本加日志时通常会记录这几个关键节点开始连接某台设备、登录成功、准备执行某条命令、命令执行完毕、设备处理完成以及任何异常分支的完整堆栈信息。这样即使脚本运行到一半出了岔子你也能根据日志迅速定位是哪台设备的哪个环节出了问题。错误处理方面核心是把每台设备的失败独立隔离。不要让一台设备的异常拖垮整个批次任务。做法是在处理函数内部捕获所有异常记录日志后继续处理下一台而不是让异常一直冒泡到最外层导致程序中断。这里给出一个通用的异常处理模板def safe_process_host(host): try: process_host(host) except Exception as e: logger.exception(f设备 {host} 处理失败跳过并继续下一条)简洁但有效。生产环境里自动化工具跑批量的第一目标是不中断单台失败要允许重试或跳过最后汇总一个报告告诉你哪几台成功了哪几台失败了。6.4 连接安全的小警示前面反复强调了这是实验或内网环境。但代码写多了难免会有人把它搬到生产去用这里必须啰嗦一句安全上的事。telnet协议传输的所有数据包括用户名和密码都是明文。也就是说如果在不受信任的网络中使用telnet中间人只要抓包你的设备密码就裸奔了。生产环境务必使用SSHPython对应的库是Paramiko用法和telnetlib非常相似等telnet实验吃透了迁过去并不困难。另外脚本代码里不要硬编码明文密码。我见过很多工程师把自己的VTY密码直接写死在脚本里脚本再通过即时通讯传来传去等于把口令撒得满世界都是。至少要把密码放到独立配置文件中设置合理的文件权限更好的是使用环境变量或密钥管理工具注入。哪怕只是自己用也要养成这个习惯。我在自己的实验脚本里一般会把密码放到config.ini并设置600权限代码从配置文件中读取而不是写死在代码里。这样做还有一个附带好处别人拿到你的脚本时看不到任何明文口令分享示例代码也更放心。7. 常见问题与排查实录7.1 我在实验中踩过的坑这节直接上干货把我自己实验过程中遇到过的所有坑按频次排个序附带解决思路给后面的人当“避坑指南”。第一个坑是域名解析卡顿。思科设备默认会在终端连接时尝试对源IP做DNS反向解析如果设备没有配置DNS服务器这个解析过程会卡到超时表现就是telnet登录后界面迟迟不出现提示符或者输入命令后感觉设备反应迟钝。解决方法是设备配置里加no ip domain-lookup或者同步配置ip domain-name。这个坑在模拟器上特别常见因为模拟器环境通常没有DNS服务一旦忽略所有登录实验都会觉得“怎么这么慢”。第二个坑是模拟器的telnet端口映射。GNS3和EVE-NG这类模拟器在桥接设备时会把设备的telnet端口映射到本机的某个高位端口而不是直接用23端口。比如GNS3里第一台设备可能是localhost:5000第二台是localhost:5001。很多人照着教程写脚本时直接连接设备IP的23端口发现怎么都连不上查了很久才发现模拟器的端口映射机制。用这类模拟器做实验时一定要先在图形界面里确认设备的telnet控制端口是哪个。第三个坑是密码提示符匹配失败。有些设备密码输错后提示符会重新变成Password:或者Login incorrect然后再来一次。如果你的脚本只匹配了一次Password:输入密码后直接往下走设备端还没认证通过后面的命令全部无效。解决方法是登录流程中在密码发送后增加一个对输出内容的检查看到Login incorrect之类的关键词就抛异常重试不要硬着头皮继续。第四个坑是字节流的编解码问题。telnetlib返回的是原始字节流decode的时候如果遇到非ASCII字符直接用errorsignore忽略可能会丢掉关键提示信息。更稳妥的做法是decode时指定errorsreplace把无法解码的字节替换成特殊字符这样至少能看到异常位置。如果设备输出里有中文乱码多半是编码格式不对需要尝试gbk或utf-8这个要看设备的语言配置。第五个坑是执行命令后read_very_eager读到空内容。不一定是设备没输出更可能是设备输出还未到达本地缓冲区你就先读了。所以read_very_eager之前给一个适当的sleep是必要的。sleep时间太短容易漏数据太长浪费效率我一般用0.5到1秒作为起点然后根据设备响应速度微调。7.2 常见故障速查表为了让大家更快速地定位问题我整理了一个常见故障速查表可以直接对照查故障现象可能原因排查和处理方法连接超时无法建立telnet连接IP不可达、端口错误、模拟器端口映射未配置先手动ping目标IP再手动telnet测试端口确认设备telnet服务开启登录后无任何提示符脚本卡在read_until设备在尝试DNS反解域名响应很慢设备配置no ip domain-lookup或增加等待超时时间提示Login incorrect用户名或密码错误手动telnet验证账号密码检查设备VTY配置能登录但无法进入特权模式enable命令或enable密码不对手动验证enable密码确认设备配置了enable secret/password命令执行后输出内容只有一半sleep时间不够缓冲区数据未到齐增大sleep或改用沉默检测收包逻辑show命令执行后脚本卡住不动设备分页显示停在--More--等待输入先发送terminal length 0关闭分页或检测--More--自动发送空格中文回显乱码decode编码不匹配尝试gbk/utf-8等编码库依次解码验证批量处理时个别设备失败拖累整体异常未捕获导致整个程序中断在设备处理函数内部捕获异常单台失败跳过并记录日志同一段代码在华为设备上命令无效命令集和交互流程不同确认设备厂商使用对应的命令集和登录提权流程这张表看起来简单但都是真实排查经验的浓缩。如果你实验遇到问题先对照现象找到对应行八成问题就解决了。7.3 调试技巧抓包和开启调试日志最后一招如果你的脚本问题始终排查不出别硬熬直接抓包看交互过程。Wireshark对telnet协议解析非常完善能看到客户端发送了哪些字节、服务端返回了哪些字节、TCP重传发生在哪个阶段甚至连设备交互的每一个字符都一目了然。我曾经遇到一个奇怪的问题脚本偶尔会在输入密码后卡住后来抓包才看到设备端在密码提示之前先发了一个空行脚本没有处理这段空数据导致读到错误的位置。这种问题光靠猜测很难定位抓包一看就明白。也可以用telnetlib自带的调试日志功能。设置tn.set_debuglevel(1)后程序会把收发数据的过程完整打印到终端配合time.sleep逐步观察每一步的状态。调试完成后把debuglevel设回0否则日志滚滚而来干扰正常输出。调试实时交互时我还习惯在关键节点临时加print语句标注“已经到了哪一步”。这个方法土但有效尤其适合卡在某个环节时快速分清是连接问题、认证问题还是命令执行问题。8. 实验完成之后的一点体会这个telnet实验做完你手上的东西可能很简单就是几十行Python代码但它背后代表的思路变化是巨大的。以前你面对设备的第一反应是打开终端、敲命令、等回显、复制粘贴现在你开始意识到所有手工操作都可以被程序复现和批量化。telnet只是入口后面无论转向SSH、NETCONF还是Restconf核心逻辑都是一个套路建立连接、认证、发命令、收结果、解析处理。地基打牢了上面盖什么楼都稳。我从这个实验里收获最大的并不是那几行能用的脚本而是理解了网络设备和程序交互的本质设备本质上就是一个文本接口的服务端你给它字符串它回你字符串自动化要做的就是把人类习惯的“看提示符、敲命令、读回显”这个循环变成代码逻辑。这个认知一旦建立后面看Netmiko、Nornir这些框架时就完全不会再觉得神秘反而能看懂它们每一层封装的用意。如果你刚跑通这个实验下一步建议两个方向一是把实验环境换成真机或更真实的模拟器增加设备数量做批量配置备份和巡检二是把通信协议从telnet升级到SSH用Paramiko重写一遍登录逻辑体会加密连接和主机密钥校验带来的额外处理复杂度。做完这两步你就能自信地告诉别人自己已经入了网络自动化的门。最后再分享一个小技巧把实验过程中所有能复用的代码整理成自己的工具模块连同样的实验脚本也不要直接复制粘到新项目。花点时间把login、send_command、close这些基础能力沉淀下来长期看节省的时间远比你想象的要多。我自己的模块从第一次写到现在改了很多版每一版都在解决实际工作中新遇到的问题这个不断打磨的过程本身就是最有价值的学习方式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑