资讯动态

Python telnetlib实现网络设备远程登录自动化运维

发布时间:2026/10/16 3:38:30 来源:尧图企业网站定制
刚接手一批网络设备管理的时候最头疼的就是每天要对着十几个交换机和路由器反复敲相同的配置命令。telnet 登上去、输入账号密码、逐条执行命令、保存配置、退出每一步都是重复劳动稍微走神还可能把命令敲错。后来我用 Python 写了一套 telnet 自动化运维脚本把整个远程登录管理流程收敛成了几段核心代码从此批量巡检、批量下发配置都变成了一键操作。这篇文章我打算把完整的实践过程重新捋一遍。从为什么选 telnet 而不是其他协议到 telnetlib 库的底层交互逻辑再到能直接拿去用的脚本模板最后是实际调试过程中踩过的坑。内容围绕“Python 自动化运维实现 telnet 远程登录管理设备”展开适合刚接触网络自动化的运维同学参考也适合已经在做命令行自动化、想补充 telnet 这块拼图的同行用来查漏补缺。1. 整体方案设计与技术选型1.1 为什么在自动化场景里还需要关注 telnet很多人一听 telnet 就觉得是老古董觉得既然有 SSH 为什么还要折腾明文协议。但实际接触过生产网络环境的人都知道telnet 在一些场景里不仅没被淘汰反而还是主流。我这边管理的设备里有一部分是多年前采购的三层交换机出厂固件只支持 telnet 管理不支持 SSH。另一部分是内网核心设备由于安全策略要求管理链路完全隔离运维网段和其他业务网段做了物理分离在保证隔离的前提下设备为了兼容旧的脚本工具链仍然保留 telnet 管理口。从自动化的角度来看telnet 有两个非常实际的优点。第一是协议极简连接建立之后就是纯文本交互没有 SSH 那套密钥交换和加密握手流程脚本逻辑更直白调试也方便。第二是兼容性极好无论是思科、华为还是某些国产网络设备telnet 命令行接口的行为模式几乎一致无非是“用户名 - 密码 - 用户视图 - 系统视图”这套流程编写起来非常顺手。当然我并不是鼓励大家无脑用 telnet。现代网络设备能开 SSH 还是优先开 SSH。但作为一个合格的运维脚本你的工具箱里必须有 telnet 这个方法因为你永远不知道下个季度采购的设备会不会又是只支持 telnet 的型号。1.2 为什么选用 Python 的 telnetlib 库Python 标准库里有一个 telnetlib 模块专门用来处理 telnet 协议。选它而不是 paramiko 或 netmiko核心原因是零依赖。在运维服务器的 Python 环境里你不需要先 pip install 任何额外包直接用标准库就能跑起来这对生产环境来说非常重要。有些内网运维服务器是离线环境装第三方包非常麻烦标准库方案能省掉很多沟通成本。另外telnetlib 的抽象层级很低。它没有像 netmiko 那样封装好固定的交互模式但这也意味着你可以完全控制发送什么字符、等待什么回显。比如某些设备的登录提示符是User name:而不是常见的Username:如果你用 netmiko可能需要通过自定义device_type才能适配但用 telnetlib 只需要改一行read_until的匹配字符串即可。底层控制力强在应对杂牌设备时优势尤其明显。当然telnetlib 的缺点也很明确它不会帮你做命令结果的分页处理、不会自动处理---- More ----也不会帮你维护一个健壮的命令通道。这些问题需要我们自己在脚本层解决后面的章节我会专门讲分页符怎么处理。1.3 自动化脚本整体结构拆解我的脚本整体可以拆成四层设备管理清单层维护设备的 IP、账号、密码、登录方式、端口号 连接控制层负责建立 telnet 连接、登录认证、检测登录是否成功 命令交互层发送命令、读取回显、处理分页、判断是否到末尾 业务编排层批量巡检、批量备份、批量下发 ACL 等具体任务这个分层的思路很直接把连接设备和执行业务命令解耦。以后你新增批量任务不需要再重复写连接逻辑只需要在业务编排层加一个函数就行。如果某一天网络改造导致登录方式变了也只需要修改连接控制层业务代码完全不用动。2. 核心细节解析登录认证与命令交互2.1 telnetlib 连接设备的底层行为telnetlib 本质上就是帮你处理了 TCP 连接的数据收发。它内部读取数据是流式的你必须明确告诉它等到哪个字符串出现再继续否则read_until会一直阻塞到超时。这块是新手最容易卡住的地方我多解释几句。当你调用tn.read_until(bPassword: , timeout5)时telnetlib 会不断读取套接字中的数据每收到一段就检查缓冲区里是否包含目标字节串如果包含立即返回包含目标串的完整回显数据如果超过 timeout 还没等到就会返回当前已经收到的所有数据但不抛出异常。这个特性非常关键我们判断登录失败时靠的就是它超时之后的返回值内容。还有一个细节是tn.write()发送数据时telnetlib 不会自动帮你加换行符。你写tn.write(bsystem\n)和tn.write(bsystem\r\n)在大多数设备上都能执行命令但少数严格模式的设备只认\r\n。我在脚本里统一用\r\n兼容性最好。2.2 登录认证的可靠实现方式telnet 登录一般有两种提示样式。旧款华为设备常见的是Username:思科老设备常见的是Username:但有不少国产设备中文提示和英文提示混杂。经验之谈是不要猜提示词而是用read_until等待一个比较宽泛的字节串然后紧接着判断当前处于什么状态。我常用的写法是import telnetlib import time def telnet_login(host, port, username, password, timeout10): tn telnetlib.Telnet(host, port, timeouttimeout) # 等待出现登录提示常见的有 Username/Login/User name data tn.read_until(bUsername:) if bUsername: not in data and bname: not in data: raise RuntimeError(f未检测到登录提示, 回显: {data[:200]}) tn.write(username.encode(ascii) b\r\n) data tn.read_until(bPassword:) if bPassword: not in data: raise RuntimeError(未检测到密码提示) tn.write(password.encode(ascii) b\r\n) # 等待登录成功的标志常见的是 ] 或设备名称 data tn.read_until(b, timeout5) if b not in data and b] not in data: raise RuntimeError(f登录可能失败, 回显: {data[:200]}) return tn这里有个细节read_until(bUsername:)如果设备的提示是User name:因为里面包含name:所以也能匹配到这就是我等宽泛字节串的原因。但要注意如果设备登录提示是纯中文的字节匹配会失效这种情况需要先登录一次拿到中文提示的编码再在脚本里用对应的中文 encode。2.3 命令下发与回显读取的机制登录成功之后命令交互就简单了。发送命令、读取回显、判断结果三个动作一个循环。但真正麻烦的是设备输出过长时出现的分页。大多数网络设备默认开启分页功能输出超过一屏就会暂停在---- More ----等待你按键继续。处理分页的策略有三种第一种是发送命令前先禁用分页。比如思科执行terminal length 0华为执行screen-length 0 temporary。这是最推荐的做法一劳永逸。但要注意部分老设备不支持关闭分页这时就用到第二种策略。第二种是遇到More就发送空格键。实现方式是循环调用read_until(b---- More ----)匹配到了就write(b )直到命令回显完全结束。这个方法比较暴力但只要设备输出里出现了这个提示符就能用。第三种是干脆不用 read_until改用定时 sleep 加固定次数读取。比如tn.write(command.encode(ascii) b\r\n) time.sleep(2) output b while True: try: chunk tn.read_very_eager() output chunk if not chunk: break except Exception: break这种方式适合那些既不支持关闭分页、又对空格响应迟钝的老设备。缺点是不够优雅但因为time.sleep给足了时间实际效果最稳。2.4 为什么需要等待标志字符串而不是固定 sleep很多初学写的 telnet 脚本是发送命令后 sleep 3 秒再读数据这种方法简单但隐患很大。网络设备执行命令的速度差异非常大display current-configuration在大配置量的设备上可能要执行半分钟而display clock可能一秒不到就完成。如果统一 sleep 3 秒慢设备的结果还没出来快设备的输出也读不全。所以原则是等待标志字符串优先sleep 兜底。比如查询命令我们可以在发送命令之后等待界面提示符再次出现比如]或这代表设备已经执行完命令并且回到了可以输入下一行命令的状态。时间上完全自适应设备执行多久我们就等多久。只有当等待提示符超时了才降级到额外 sleep。3. 实操过程与完整脚本实现3.1 批量设备登录的脚本骨架我这边实际使用的脚本会先从文本文件读取设备信息然后逐台连接执行命令。文本文件的格式非常简单192.168.10.1|admin|pass123|huawei 192.168.10.2|admin|pass456|cisco 192.168.10.3|admin|pass789|generic每行字段用竖线分隔分别是 IP、用户名、密码、设备类型。之所以在文件里带设备类型是因为不同品牌的命令不同。同样是查看设备名称华为是display current-configuration | include sysname思科是show running-config | include hostname。脚本里根据设备类型选择不同的命令集。批量处理的核心逻辑如下import telnetlib import datetime def process_device(ip, username, password, device_type): tn telnet_login(ip, 23, username, password) commands get_commands_by_type(device_type) results {} for cmd in commands: output send_command(tn, cmd) results[cmd] output save_to_file(ip, results) tn.close()这段代码看起来简单但生产环境里有一个非常耗时的环节每台设备连接一次登录认证要 2 到 5 秒执行多条命令再算上网络往返一台设备跑完大约需要 20 到 40 秒。如果你有 50 台设备串行处理就要 20 分钟以上。这时候就需要引入并发但并发又会带来一个新的问题后面的常见问题章节我会专门讲多线程并发登录的结果错乱是怎么解决的。3.2 发送命令的封装与结果解析send_command函数不要直接裸写tn.write()加read_until()因为你需要同时处理命令回显里包含命令本身和结果末尾出现提示符这两个特征。注意把命令本身回显剔除是一个容易踩坑的细节。设备默认开启 echo你在终端里输入什么设备就会回显什么。所以如果你执行display version收到的第一个字符块就是display version\r\n然后才是命令结果。这个回显对我们判断有没有发送成功有帮助但如果要归档日志就得把它去掉。我的处理方式是发送命令后先read_until(bdisplay version)把命令回显消耗掉再继续读取后续输出直到提示符。这样最终存放在字典里的结果就不包含命令回显干净很多。当然这里有个反例。有些设备在注销后会重新显示登录界面或者命令执行过程中设备主动重启都会导致等待提示符超时。这属于设备异常我会单独打标不塞进正常结果里。def send_command(tn, cmd, promptb], timeout15): tn.write(cmd.encode(ascii) b\r\n) data tn.read_until(cmd.encode(ascii), timeout5) # 此时已经读到了命令回显 result b while True: try: chunk tn.read_until(prompt, timeout) result chunk if prompt in chunk: break except Exception: # 超时兜底读取当前所有数据 result tn.read_very_eager() break # 剔除最后那个提示符自身 result result.replace(prompt, b) return result3.3 检测设备连通性与登录失败的快速跳过批量处理时首先要做的是连通性预检。脚本开局会对所有设备发送一个 TCP 连接测试。如果端口不通直接标记为离线不进入登录逻辑。这一步可以避免登录阶段的大量超时等待。TCP 预检可以用 socket 实现也可以用 telnetlib 的Telnet()构造直接 try except。我的实现是 socket 连接测试import socket def check_port(ip, port, timeout3): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) try: s.connect((ip, port)) return True except Exception: return False finally: s.close()如果预检通过但登录失败脚本不会硬等。登录函数里检测到没有出现标志字符串就直接抛异常外层捕获后记录日志继续处理下一台设备。3.4 配置备份场景的实操脚本下面我给一个可以直接改改就用的完整备份脚本。功能是登录一批华为交换机依次执行关闭分页、进入系统视图、执行display current-configuration把输出保存到以 IP 和日期命名的文件里。import telnetlib import datetime import os HOSTS [ {ip: 192.168.10.1, user: admin, pwd: pass123}, {ip: 192.168.10.2, user: admin, pwd: pass456}, ] LOG_DIR config_backup def login(ip, user, pwd): tn telnetlib.Telnet(ip, 23, timeout10) tn.read_until(bUsername:) tn.write(user.encode() b\r\n) tn.read_until(bPassword:) tn.write(pwd.encode() b\r\n) # 登录成功标志通常是 ] tn.read_until(b], timeout5) return tn def backup_one(host): ip host[ip] tn login(ip, host[user], host[pwd]) # 关闭分页 tn.write(bscreen-length 0 temporary\r\n) tn.read_until(b], timeout5) # 进入系统视图再执行 display current-configuration tn.write(bsystem\r\n) tn.read_until(b], timeout5) tn.write(bdisplay current-configuration\r\n) config b while True: try: chunk tn.read_until(b], timeout10) config chunk if b] in chunk: break except Exception: config tn.read_very_eager() break # 去掉末尾的提示符 config config.replace(b], b, 1) # 保存文件 if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) fname f{ip}_{datetime.datetime.now().strftime(%Y%m%d_%H%M%S)}.cfg with open(os.path.join(LOG_DIR, fname), wb) as f: f.write(config) tn.close() print(f[OK] {ip} 备份完成) for h in HOSTS: try: backup_one(h) except Exception as e: print(f[FAIL] {h[ip]} 失败: {e})这个脚本我实际跑过很多次有一个要点值得单独讲screen-length 0 temporary后面必须等待]出现再发下一条命令。因为设备处理命令是串行的前一条没执行完后一条命令可能会被吞掉或者挤在同一个输入缓冲区里导致前一条命令被截断。脚本里的每个read_until都是在强制保证时序。3.5 结果日志与报告生成批量运维脚本跑完如果没有清晰的报告那等于白做。我的脚本会把每台设备的执行结果汇总成一个纯文本报告标记出三档状态OK登录成功、命令执行成功、回显完整 WARN登录成功但某条命令回显缺失 FAIL登录失败或连接超时报告生成逻辑不复杂在 process_device 返回结果后统一写进一个本地横线分隔的日志文件。文件内容大概长这样 设备: 192.168.10.1 时间: 2024-06-01 10:30:22 状态: OK 命令: display version 回显前100字节: VRP (R) software, Version 5.130 有了这个报告就算脚本交给别的同事跑也能直接看懂整体巡检情况。4. 常见问题与排查技巧实录4.1 登录提示符不匹配导致一直超时这是最常见的故障。网络设备厂商非常多登录界面的提示词五花八门。有的显示Username:有的显示login:有的甚至直接显示User name:如果你写的只有一个固定匹配串很容易卡死在等待阶段。我的排查思路是先用普通 telnet 客户端手动连一次设备把真实的登录提示字符串原样拿下来再回到脚本里调试。如果实在拿不准可以用read_until(bname:)这个折中方案能同时匹配Username:、User name:、login:覆盖度高很多。不过要小心某些设备在输入用户名错误之后退回到login:提示这也可以用来辅助判断账号是否错误。4.2 禁用分页命令不生效华为设备上screen-length 0 temporary一般是临时生效的但有些设备对这条命令的支持有差异比如需要指定屏幕行数加零有些设备实际参数是screen-length 0不带 temporary。思科设备则是terminal length 0两者不同。如果在测试时发现发送禁用分页命令后回显依然出现---- More ----先不要急着改脚本打开调试模式手动执行一次。telnetlib 有个tn.set_debug_level(1)方法开启后所有收发内容会打印到终端。用这个方法去确认设备到底有没有收到命令、有没有返回错误提示通常问题立刻就能暴露出来。4.3 命令回显中包含异常字符或乱码telnet 协议本身还会传输一些控制字符比如\xff\xfb\x01这种 IAC 序列。telnetlib 内部已经做了处理一般不会出现在read_until返回的数据里。但某些国产设备在登录后会输出一些 ANSI 颜色转义序列或者制表符看起来像乱码但对后续判断影响不大。归档日志时如果想清理可以用正则去掉 ANSI 转义。如果设备使用中文语言包回显里还有可能包含 UTF-8 中文。这时读取数据时要特别注意在 Python 3 里这些都是 bytes你用decode(utf-8)转成字符串时如果碰到半个字节残留就会报错。我的解决办法是先用bytes.decode(utf-8, errorsreplace)做兜底解码保证日志不会因为单个字符异常而中断。4.4 多线程并发时结果错乱的问题曾经我把脚本改成多线程并发执行每台设备一个线程结果发现日志文件里不同设备的内容互相穿插。原因很简单多个线程同时往同一个文件里写没有加锁。这个问题排查看起来复杂其实解决办法很粗暴每个线程生成的报告先写在内存里最后统一由主线程汇总写入文件。或者用threading.Lock包住写入操作。我在生产脚本里用的是前者因为日志采集量不大内存完全能承受。另外一个并发隐患是设备侧本身不允许同一账号同时登录多个会话。部分老设备检测到重复登录会强制踢掉之前那个会话导致后面的任务全部乱掉。所以并发数量我一般控制在 5 个以内并且同一个设备文件里的账号尽量错开。更稳妥的方案是每台设备使用独立运维账号。4.5 密码配置与敏感信息保护telnet 本身就是明文协议所以脚本里的明文密码就更加敏感。我在生产环境中不会把真实密码直接写在脚本或者设备清单文件里而是用一个简单的环境变量注入方式。脚本运行时从环境变量读取密码比如os.environ.get(NET_DEVICE_PASSWORD)。这样即使脚本被传到其他地方拿不到环境变量也跑不了核心逻辑。另外设备清单文件建议设置严格的文件权限只允许运维账号读取。如果公司有密钥管理系统也可以先把密码加密存放到系统里脚本启动时临时解密用完即焚。这部分属于安全工程范畴根据公司安全策略实施即可但底线是脚本里绝对不能出现真实生产密码。4.6 命令执行超时但设备其实还在处理有时候你等待提示符]等了 20 秒没等到但实际上命令还没执行完。比如设备正在保存配置或者正在执行某些耗时操作回显停顿是正常的。这种情况下直接判失败会误报。我的改进策略是增加重试机制。第一次read_until超时后不要立即抛异常先读取一次read_very_eager()看看是否有部分输出如果有说明命令还在执行中继续等待下一轮。只有连续多轮都读不到任何新数据才判定命令卡死。5. 个人实操建议与扩展方向这套 telnet 自动化方案我已经在真实环境里稳定跑了一年多定期批量备份、批量巡检、批量修改设备密码都靠它。用下来最大的体会是telnetlib 虽然功能简单但配合等待标志字符串、处理分页、统一分层这三个核心思路能解决绝大多数网络设备自动化问题。而且标准库没有额外依赖扔到任何一台内置 Python 的运维机上就能运行这是它最大的优势。如果后续想在这个基础上更进一步建议留意两个方向。第一个方向是引入 Netmiko 这样的高阶库它其实就是对底层 telnet/ssh 通道的进一步封装内置了大量设备类型的交互模板适合需要兼容上百种品牌设备的大型环境。第二个方向是把自己的脚本改造成服务化接口通过 API 方式接收设备清单和任务指令让其他同事也能自助触发远程备份和巡检不必再直接操作脚本。最后分享一个我实践出来的小技巧telnet 脚本调试前期建议在关键步骤之间多插入print(repr(tn.read_very_eager()))直接打印原始字节数据。很多诡异的问题都是回显格式和我们想象的不一样导致的看一眼原始数据立刻就知道怎么改匹配串了。保持这个调试习惯telnet 自动化这块会顺畅很多。

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

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

免费获取报价 →
↑