资讯详情

资讯详情

Python通过Telnet连接路由器:网络设备自动化运维实战

1. 项目概述与核心需求拆解1.1 这个需求从哪里来搞网络运维的人尤其是跟路由器、交换机整天打交道的那批大概率都经历过这种场面早上刚到工位领导丢过来一台新到的路由器说配置一下下午要用或者月底要巡检几十台设备一台一台登录进去敲display version、show ip interface brief手都敲酸了再或者某个分支机构的设备出了故障人过不去只能远程登录看一眼状态。这个时候如果有个脚本能替你把设备连上、把命令敲完、把结果抓回来那就舒服太多了。python-通过telnet连接路由器这个项目就是把手工登录设备执行命令这个动作封装成一串可以反复调用的代码。核心诉求就一句话用Python替我做掉那些重复、枯燥、容易出错的网络设备登录与命令执行流程。1.2 这个项目到底能做什么说得再直白一点这个脚本能帮你完成三件最基础也最刚需的事。第一件事自动化登录设备。设备地址、端口、用户名、密码提前配好脚本启动后自动完成TCP连接、telnet协商、登录认证整个过程不需要人坐在电脑前盯着屏幕敲键盘。第二件事批量执行命令。登录进去之后想干嘛就干嘛查看路由表、抓配置、改接口状态、保存配置反正平时手工敲什么命令脚本就能替你发什么命令。第三件事回显抓取与保存。设备返回的每一行输出脚本都能抓下来存成文件方便后续分析、留档、对比配置差异。有人可能会问现在大家不是都用SSH了吗telnet还有必要搞吗这个想法对了一半。新部署的设备确实是SSH为主但大量存量设备——尤其是内网环境里的老设备、工控网络里的专线设备、实验室里的测试机——往往只开了telnet服务或者出于兼容性考虑一直用telnet。另外不少设备telnet登录后能直接用enable、config terminal这些命令调试时比SSH还顺手。所以掌握Python操作telnet这套方法在真实工作里依然非常有价值。1.3 谁适合看这篇内容这篇内容适合网络运维工程师、系统管理员、刚接触Python自动化的大学生以及那些会登录设备但不会写脚本和会写Python但没碰过网络设备的两类人。前者缺的是把网络知识转成代码的能力后者缺的是跟设备打交道的经验。这篇内容把两边都照顾一下底层原理讲清楚代码怎么组织说明白踩过的坑也全部列出来。2. 方案选型与技术原理拆解2.1 为什么用telnetlib而不是其他方案Python要去跟网络设备做telnet交互可选的库其实就那么几个。最出名的当然是标准库里的telnetlibPython 2时代就有了Python 3里继续用。它的好处是零依赖Python装好就能import不需要装额外的第三方包缺点也很明显——API偏底层交互逻辑要自己写没有现成的登录设备、等待回车这样的高级封装。另外有一个叫pexpect的库也能做类似的事它擅长处理交互式命令行程序通过匹配屏幕输出来决定下一步动作。在Linux下用pexpect去控制telnet、ssh这类子进程非常顺滑对比telnetlib来说它的匹配逻辑更接近人的思考方式——看到某个提示符就发某个命令。但pexpect的问题在于它是第三方库而且对Windows的支持不够友好Windows环境下跑pexpect会有不少额外工作。还有一条路是用paramiko但它本身只封装SSH协议跟telnet一点关系都没有。所以如果你的目标设备只支持telnetparamiko帮不上忙。反过来讲如果设备同时支持SSH和telnet我个人的经验是直接上paramiko——SSH加密、可压缩、能传文件安全性高一个量级。但本项目的目标场景就是telnet因此标准库telnetlib是首选。提醒一下telnetlib在Python 3.13里已经被标记为弃用预计未来某个版本会移除。如果是长期项目建议把核心交互封装成单独模块后续替换成第三方库比如telnetlib3时改动面小一些。不过就当前主流Python 3.6到3.11环境而言telnetlib完全够用这篇内容里的代码也以它为准。2.2 telnet协议本质一场半双工的文本对话很多人刚接触telnet编程时最容易犯的错就是把telnet想象成跟HTTP一样发一个请求收一个响应。实际上telnet协议根本不是这个思路。telnet的底层就是TCP连接默认端口23。服务端和客户端之间不是简单的一问一答而是双方把字节流往对方那里丢屏幕上的所有输出本质上是字节流。换句话说你看到的登录提示符、密码提示符都不是协议层面专门定义的字段它们就是设备发送过来的普通文本恰好显示在了终端上。协议里真正有特殊定义的是一批被称作telnet命令的控制字节比如IACInterpret As Command、WILL、WONT、DO、DONT等用来协商终端类型、窗口大小、回显开关这类选项。理解这一点对写代码非常关键你用read_until等待某个字符串出现本质上是在字节流里找特征子串。你等的是路由器返回的Username:或Password:这样的提示文本而协议协商的那堆控制字节telnetlib已经帮你在底层处理掉了不需要自己操心。这正是telnetlib的价值——它把IAC序列解析、选项协商全包了暴露给开发者的是一个流式的读接口。成也流式败也流式。因为设备输出是持续不断的流不像HTTP响应那样有明确的结尾标记所以脚本里必须明确告诉它我等哪一段字符出现才算结束。这是整个项目里最需要打磨的地方后面会专门讲。2.3 登录流程里容易被忽略的环节telnet登录路由器表面上就三步TCP连接、输用户名、输密码。但实际写代码你就知道细节藏在每一步的等待里。首先是TCP层面。设备刚开机时端口还没就绪立即连接大概率失败。这个在编码里要设计重试机制尝试连接三次每次间隔三五秒。其次是登录提示。设备发过来Username:之后你输入用户名的时机不对或者read_until设置的等待时间太短都会造成登录失败。这里有个经验值telnet登录提示的等待时间不能设短了我用下来以5秒到10秒为基准老旧设备可能需要更长。密码发送之后也一样设备要验证身份可能要一两秒。验证通过后进入用户模式或特权模式设备会打出各自的提示符比如思科的Router或者华为的Huawei。这个提示符就是你判断登录成功的标志。而且注意很多设备的提示符支持改写生产环境里网络工程师经常会把提示符改成设备名加上编号写脚本时不能用写死的提示符去等最好是等一个包含设备名关键词的模糊模式或者干脆等一个特征命令的输出。这些细节新手通常意识不到。等到脚本在真机上一跑要么卡在read_until里要么登录成功了却不知道下一步该干嘛这才回头补课。3. 核心代码实现与操作步骤3.1 环境准备与依赖清单这个项目在环境层面的要求非常低列表如下依赖项版本建议说明Python3.8 ~ 3.113.12基本兼容3.13要留意telnetlib弃用提示telnetlib标准库自带无需安装操作系统Windows / Linux / macOS均可telnetlib是跨平台的目标设备支持telnet协议的网络设备路由器、交换机、部分防火墙写代码之前先在工作机上验证一下能不能跟设备建立telnet会话这一步能排除大量网络层面的干扰。如果是Windows命令行里执行telnet 192.168.1.1能看到登录提示就说明网络通、端口开。Linux/macOS也一样操作。注意一点Windows 10以上系统默认没装telnet客户端需要在启用或关闭Windows功能里勾选Telnet客户端或者用Python脚本直接探一下端口也行。# 探端口通不通比直接telnet更轻量 python -c import socket; ssocket.socket(); s.settimeout(3); s.connect((192.168.1.1, 23)); print(port open)3.2 完整实现从登录到命令执行下面这段代码就是这个项目最基础、最核心的版本。目标场景是已知路由器地址、用户名、密码登录后依次执行几条命令并把回显保存到本地文件。import telnetlib import time def telnet_connect(host, username, password, port23, timeout10): 建立telnet连接完成登录返回telnet客户端对象 try: tn telnetlib.Telnet(host, port, timeout) except Exception as e: print(f连接失败: {host}:{port} - {e}) return None # 等待用户名提示 tn.read_until(bUsername:, timeout) tn.write(username.encode(ascii) b\r\n) # 等待密码提示 tn.read_until(bPassword:, timeout) tn.write(password.encode(ascii) b\r\n) # 登录成功后等一下再读先把提示符的残余输出吸干净 time.sleep(1) try: banner tn.read_very_eager() if banner: print(b登录回显: banner) except Exception: pass return tn def run_commands(tn, commands, wait_word, timeout10): 依次执行命令列表每个命令等待wait_word出现为止 results [] for cmd in commands: tn.write(cmd.encode(ascii) b\r\n) output tn.read_until(wait_word.encode(ascii), timeout) results.append(output.decode(utf-8, errorsignore)) return results if __name__ __main__: HOST 192.168.1.1 USER admin PASS admin123 CMD_LIST [display version, display current-configuration] WAIT ] # 有些设备提示符包含]临时用这个字符作为命令结束标志见下文说明 tn telnet_connect(HOST, USER, PASS) if not tn: exit(1) outputs run_commands(tn, CMD_LIST, WAIT) with open(router_output.txt, w, encodingutf-8) as f: for i, (cmd, out) in enumerate(zip(CMD_LIST, outputs)): f.write(f 命令: {cmd} \n) f.write(out) f.write(\n) tn.close() print(执行完成结果已保存到 router_output.txt)3.3 提示符匹配的细节等什么字符串是最大学问这段代码里最难拿捏的不是登录而是run_commands里的wait_word参数。它决定了脚本发完一条命令后要等到设备输出里出现什么字符才算命令执行完了可以发下一条了。常见几种思路第一种等提示符的结尾字符。对华为设备提示符类似Huawei那就可以等b。对思科设备提示符类似Router可以等b。对于启用配置模式后的提示符例如Router(config)#那就要等b#。这是大多数教程给的做法但注意一个问题如果命令本身的输出里恰好包含或#字符read_until会在半路就提前结束。比如display current-configuration输出的配置内容里接口配置段落里满屏都是#等#就完蛋了命令还没执行完就被拦腰截断。第二种等一个设备名关键词。把提示符改写为包含设备名比如Router1那么等bRouter1就比单独等b可靠得多。因为设备名在输出文本里很少出现误匹配概率大降。缺点是为不同设备改提示符这件事本身需要额外操作。第三种等一个明确的命令结束标志。这个最稳但实现稍微绕一点发完目标命令后紧接着再发送一个专门用来探路的命令比如敲一个回车或者发一条echo CMD_END_12345然后等这个独一无二的字符串出现。因为输出内容是设备实时生成的只有前面那条命令执行完了后面这条探路命令的输出才会出现所以等到了自定义标志就代表前面的命令已经执行完。这个方法是我最推荐的稍后详细展开。3.4 进阶方案带分页处理与探路命令实际设备执行display类命令时输出一长就会分页最典型的就是---- More ----停在屏幕中间等你按空格或回车继续翻页。脚本如果不管这个read_until等待的特征串可能永远不出现最后超时退出。处理分页有两类做法。一类是在设备侧设置登录后先执行关闭分页的命令比如华为设备是screen-length 0 temporary思科设备是terminal length 0执行完后所有输出一次性倒出来脚本就不用管分页了。另一类是脚本侧读输出每当出现---- More ----就主动发送一个空格或回车来翻页直到不再出现为止。这里给出一个改进版的执行函数把探路命令和分页处理都做进去import telnetlib import time class RouterTelnet: def __init__(self, host, username, password, port23, timeout10): self.host host self.username username self.password password self.port port self.timeout timeout self.tn None def login(self): self.tn telnetlib.Telnet(self.host, self.port, self.timeout) self.tn.read_until(bUsername:, self.timeout) self.tn.write(self.username.encode(ascii) b\r\n) self.tn.read_until(bPassword:, self.timeout) self.tn.write(self.password.encode(ascii) b\r\n) time.sleep(1) # 清掉登录后的banner try: self.tn.read_very_eager() except Exception: pass # 关闭分页 self.tn.write(bscreen-length 0 temporary\r\n) time.sleep(0.5) self.tn.read_very_eager() def exec_command(self, cmd, wait_wordNone, timeout10): 执行单条命令返回完整输出 marker CMD_END_PROBE_7890 self.tn.write(cmd.encode(ascii) b\r\n) # 发探路命令用于判定前一条命令执行完毕 self.tn.write(b\r\n) # 先补一个回车防止输出中途卡住 self.tn.write(fecho {marker}\r\n.encode(ascii)) output self.tn.read_until(marker.encode(ascii), timeout) # 去掉探路命令本身的回显和标志行 output output.split(marker.encode(ascii))[0] return output.decode(utf-8, errorsignore) def close(self): if self.tn: self.tn.close()这个exec_command的思路就是执行目标命令后不直接等特征串而是追加一个专属探路命令等它的返回值。因为探路命令的执行一定排在目标命令之后所以只要探路标志出现就说明目标命令已经完整输出完了。这个方案对输出里包含任意字符都不敏感彻底规避了提示符误匹配的问题。3.5 编码问题别小看中文设备回显网络设备来源五花八门不少国产设备回显里带着中文比如设备负载过高、接口状态异常。这些字符在telnet传输过程中编码可能是GBK或者UTF-8如果统用ascii解码就会乱码或者报错。解决思路是发送命令时统一用ascii——命令本身是英文的不存在编码问题。但接收回显时解码要灵活优先用UTF-8发现异常再用GBK重试。上面代码里errorsignore是兜底但更好的做法是封装一个解码函数def smart_decode(data): try: return data.decode(utf-8) except UnicodeDecodeError: return data.decode(gbk, errorsignore)这在做设备回显保存到文件时尤其重要。中文乱码不仅影响阅读还会干扰后续用正则提取关键信息那一步所以最好在入口就把编码问题解决掉。4. 真实场景中的操作流程与扩展应用4.1 批量巡检一次登录执行多条命令真正到了工作场景里你不会只连一台设备。一批设备几十台每台要执行十条命令还要求输出结果分别保存这正好是脚本的用武之地。方法很简单把设备信息地址、用户名、密码放在一个文本文件里或者放在一个结构简单的清单里脚本循环读取逐台建立连接执行命令把输出写到以设备IP或设备名为文件名的独立文件里最后汇总所有文件里的关键信息做成一个巡检结果表格。设备清单文件格式可以是这样192.168.1.1 admin pass123 192.168.2.1 admin pass456 192.168.3.1 root root123然后脚本里这样用def batch_run(device_file, cmds, output_dir): with open(device_file, r) as f: for line in f: parts line.strip().split() if len(parts) ! 3: continue ip, user, passwd parts r RouterTelnet(ip, user, passwd) try: r.login() results [] for c in cmds: out r.exec_command(c) results.append(out) # 保存到以IP命名的文件 with open(f{output_dir}/{ip}_backup.txt, w, encodingutf-8) as out_f: out_f.write(f设备: {ip}\n) for c, o in zip(cmds, results): out_f.write(f\n---------- {c} ----------\n) out_f.write(o) except Exception as e: print(f设备 {ip} 处理失败: {e}) # 记录失败日志方便事后人工处理 with open(f{output_dir}/failed.log, a) as fail_f: fail_f.write(f{ip} {e}\n) finally: r.close()这里有两个细节。一是失败情况要单独记录不能因为一台设备登录失败就把整个巡检断掉巡检这种场景最忌讳一个人生病全队停摆。二是每台设备处理完要close释放连接不然几十台设备连着文件描述符会被耗尽。4.2 配置变更把人为误操作的隐患关进笼子里巡检只是读取信息。还有一个更常见也更敏感的需求批量下发配置变更。比如要给全网几十台设备统一改一个SNMP参数或者下发一条新的静态路由。手工一台台敲容易敲错、漏敲人还不能走开。脚本干这事特别合适。但在这种场景里经验告诉我必须做三件事第一执行前先备份当前配置到本地。万一变更失败或误操作有回退依据。第二执行完变更命令后马上执行一遍验证命令比如确认接口状态、路由表条目看看是否符合预期。第三脚本里要有只输出到日志不打断任何交互的纪律。所有过程中的命令回显、设备反馈、错误信息全部落盘。出了事能回溯追责时也能说清楚。这里尤其注意命令顺序。有些设备对命令的执行有依赖关系比如先删旧配置再添加新配置顺序反了会报错或者配置文件里出现临时不一致。所以命令列表在设计时就要按先回退后新增、或者先关闭再开启的逻辑来组织。不带脑子的循环执行有时比手工敲还危险。4.3 定时任务与告警联动把telnet脚本放到定时任务里可以实现无人值守的定期巡检。Linux下用crontabWindows下用计划任务程序。比如每天凌晨2点执行一次把全网络设备的状态抓一遍回显存到服务器上。再进一步把抓回来的关键指标用正则提取出来得到接口流量、CPU占用率、内存使用率这些数据超过阈值就支持后续告警联动。写这块的时候有几个点必须考虑清楚定时任务的执行时间是否会和手工维护窗口冲突脚本执行时间是否足够短比如每台设备连接不超过60秒不然一批接一批顺序执行几十台下来时间很长设备侧是否需要配合开启telnet白名单只允许运维网段访问23端口。安全防护这件事从项目的第一版就应该考虑而不是等出事了再补。5. 常见问题、排查心得与避坑实录5.1 连接超时与密码验证失败我把最常见的失败场景罗列一下你自己对照排查现象可能原因排查方法Connection timed out地址错误/端口未开/防火墙拦截telnet命令手动连一下试试socket.gaierror主机名无法解析用IP代替主机名读到Username:后没反应用户名输入错误设备反复提示手动验证账号密码登录成功但命令没输出分页导致卡在More登录后先执行关闭分页命令卡在read_until超时等待的特征串不会出现改用探路命令方案回显乱码编码不匹配智能解码处理密码验证失败这个坑很有意思。有些设备为安全考虑密码输入错误时会延迟个几秒再重新提示脚本这时候如果你用read_until(bPassword:, timeout)去等新的密码提示就会卡在那里后面逻辑全乱。遇到这种情况常规做法是登录失败三次后放弃直接报错退出。代码里可以通过循环读取Username:或Password:的次数来判断认证失败超过三次就不重试了避免脚本在那里空转。5.2 设备回显字符集不一致的处理我踩过的最痛的一个坑是某台设备回显里带了一个特殊字符恰好是read_until等待字符串的前缀导致脚本提前认为命令执行完了。举例来说如果等待字符串设成b而命令输出里有类似接口状态: UP 1000M这样的行那出现两次收发混乱。用探路命令方案就是为了根治这个问题。另外一个补充措施是所有命令回显在保存之前做一次简单的清洗把可能干扰后续解析的终端的控制字符过滤掉。import re def clean_output(text): # 去掉常见的终端控制字符 text re.sub(r\x1b\[[0-9;]*[a-zA-Z], , text) text text.replace(\r\n, \n) return text5.3 不要踩telnet明文传输的雷最后必须说一句安全层面的经验之谈。telnet本身是明文协议用户名密码在网络上是裸奔的。如果设备在一个不可信的网络里再用telnet去管理设备账号密码被中间人截获后果不用我多说。所以我实际项目中的处理原则是这样如果设备位于边界网络或者不受控网络禁止用telnet改用SSH如果实在只能用telnet必须保证设备管理网段与业务网段隔离操作终端位于可信环境内并且密码定期轮换。另外脚本文件里不要硬编码口令至少要用环境变量或单独的配置文件来存放配置文件做好权限控制不能让无关人员拿到。这个项目既然叫通过telnet连接路由器那就得对telnet的短板心里有数它适合内网、适合测试环境、适合设备只支持telnet时的应急管理方式但不适合当成长期大范围管理手段。项目可以开发到能跑能用这一步但生产环境的准入标准以及安全防护的成熟度直接决定了这个方案能不能长期运行下去。5.4 脚本稳定性设计的几条经验在实际使用中我发现脚本稳定性的价值其实大于功能花哨。有几个习惯养成了之后掉链子的次数直线下降。第一个习惯所有网络操作都设超时并且超时时间不是随手填的。登录等待给10秒命令执行给10秒批量场景里给每台设备加一个总处理时间上限比如60秒。这样即使某台设备卡死了也不会让整个批量任务挂住。第二个习惯日志记录尽量详细。每条命令发出前打一条日志命令回显回来打一条日志登录成功打一条日志。到了排查的时候这些日志就是破案的线索。第三个习惯用try/finally保证连接必然释放。telnet连接如果忘了关闭在Windows上会因为句柄被占用导致脚本越来越慢Linux上则累积到文件描述符上限后报Too many open files。所以close一定放在finally里。第四个习惯脚本参数不要写死在代码里。设备清单、命令列表、超时时间、保存目录最好都能从外部配置文件读取。这样部署到不同现场时改配置文件就行不用改代码对非开发人员也很友好。6. 项目后续扩展与个人经验总结6.1 可以从telnet平滑切换到SSH的设计思路如果你觉得telnet这件事终究要往前走一步那一定要在自己的代码结构里留好接口。设计思路上把登录、执行命令、关闭连接这三个操作定义成统一的方法底层实现先用telnetlib将来条件允许时换paramiko上层调用方完全不感知。后面如果真的切换到SSH代码逻辑上几乎所有地方都能复用命令的组织方式、回显的处理、日志记录逻辑、配置文件格式这些都没变只是传输载体从telnet换成了SSH。提前做这个设计将来迁移成本会低很多。6.2 把命令交互逻辑抽象成模板再往后走一个阶段你会发现不同品牌设备的命令集不一样。华为和思科的查看命令、配置命令各有差异。一个思路是做一个命令模板库每类设备一份命令清单文件脚本根据设备类型加载对应的命令集。这样新增设备品牌时不用改脚本只加一个命令模板文件就行。这块我可以给你一个具体的组织建议把命令抽象成带占位符的模板比如CMD_TEMPLATE { huawei: { set_hostname: sysname {name}, shutdown_interface: interface {intf}\nshutdown, }, cisco: { set_hostname: hostname {name}, shutdown_interface: interface {intf}\nshutdown, } }这样上层业务只需要传参数变量不需要关心具体设备型号的语法差异。隐藏逻辑在模板里去适配设备职责清晰。6.3 最后一些想说的话做了这么多年网络自动化的东西我最大的体会是写脚本练就的能力本质上是对设备行为的理解而不是对某一段代码的背诵。telnet这套东西看着老但它教会了你怎么跟设备进行实时交互、怎么处理字节流、怎么处理异常这些能力换到SSH、换到Netmiko这些新工具里一样用得上。所以说如果哪天你要做一套更大规模的网络自动化平台今天这个telnet脚本绝不算白做。它是一块非常好的跳板。弄懂底层原理之后再接触任何基于网络协议的自动化工具都只是把怎么连和怎么等换成更舒服的封装而已。这个项目的天花板并不高但作为入门的练手项目它把Python网络编程、字节流处理、异常处理、配置文件化、批量任务管理这些关键点全部覆盖了一遍。认认真真写一遍、跑一遍、再踩几个坑你对用代码操作网络设备这件事心里就有底了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →