《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。
NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。
由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注
本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注
在很多网工学习网络自动化这条路上,netmiko是一座绕不开的小山丘,之前的文章为大家细致讲解Netmiko的很多细节,结合代码为大家带来一个比较全面的攻略,本文则更关注于一些在实际使用中遇到的“疑难杂症”,具有非常好的闭坑效果,且有一定的扩展性和一定的深度。ps: 一些Python基础技能在文末也有教程~
netmiko基础篇:最强Netmiko攻略——两万字吐血整理,网工玩转自动化
3.6 新手使用Netmiko遇到的疑难杂症
新手在使用netmiko中总是会遇到形形色色、各式各样的问题,这些问题给我们带来了很多困惑。本章节我们将为大家讲解一下新手常遇到的问题以及解决方案。
3.6.1 开启netmiko的debug模式
在讲解问题之前,首先我们需要学会开启netmiko的debug模式,将netmiko底层运作的相关信息输出出来,同时也建议在初始化netmiko连接的同时将整个session进行日志记录。这两个方法有助于我们快速定位问题。如果开启debug模式,对于大家有困难,我们也可以选择后者,常见问题在netmiko的session日志当中也比较容易定位。
import logging
from netmiko import ConnectHandler
logging.basicConfig(level=logging.DEBUG)
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'session_log': 'session.log'
}
with ConnectHandler(**dev) as conn:
output = conn.send_command(command_string='display version')
print(output)
我们在控制台会看到如下输出。
DEBUG:paramiko.transport:starting thread (client mode): 0xbd945c70
DEBUG:paramiko.transport:Local version/idstring: SSH-2.0-paramiko_2.11.0
DEBUG:paramiko.transport:Remote version/idstring: SSH-2.0--
INFO:paramiko.transport:Connected (version 2.0, client -)
...
DEBUG:paramiko.transport:=== Key exchange agreements ===
...
DEBUG:paramiko.transport:=== End of kex handshake ===
DEBUG:paramiko.transport:kex engine KexNistp256 specified hash_algo <built-in function openssl_sha256>
DEBUG:paramiko.transport:Switch to new keys ...
DEBUG:paramiko.transport:Adding ecdsa-sha2-nistp521 host key for 192.168.137.201: b'2addd778b9eb479be6a117778405b45a'
DEBUG:paramiko.transport:userauth is OK
INFO:paramiko.transport:Authentication (password) successful!
DEBUG:paramiko.transport:[chan 0] Max packet in: 32768 bytes
DEBUG:paramiko.transport:[chan 0] Max packet out: 32768 bytes
DEBUG:paramiko.transport:Secsh channel 0 opened.
DEBUG:paramiko.transport:[chan 0] Sesch channel 0 request ok
DEBUG:paramiko.transport:[chan 0] Sesch channel 0 request ok
DEBUG:netmiko:Pattern is: ((Change now|Please choose))|([\]>]\s*$)
DEBUG:netmiko:_read_channel_expect read_data:
Warning: The initial password poses security risks.
The password needs to be changed. Change now? [Y/N]:
DEBUG:netmiko:Pattern found: ((Change now|Please choose))|([\]>]\s*$)
Warning: The initial password poses security risks.
The password needs to be changed. Change now? [Y/N]:
DEBUG:netmiko:write_channel: b'N\n'
DEBUG:netmiko:read_channel: N
Info: The max number of VTY users is 5, the number of current VTY users online is 1, and total number of terminal users online is 2.
The current login time is 2022-07-18 23:31:24.
The last login time is 2022-07-11 22:43:41 from 192.168.137.3 through SSH.
<netdevops>
DEBUG:netmiko:Clear buffer detects data in the channel
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:write_channel: b'\n'
DEBUG:netmiko:read_channel:
<netdevops>
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:write_channel: b'\n'
DEBUG:netmiko:read_channel:
<netdevops>
DEBUG:netmiko:prompt: netdevops
DEBUG:netmiko:In disable_paging
DEBUG:netmiko:Command: screen-length 0 temporary
DEBUG:netmiko:write_channel: b'screen-length 0 temporary\n'
DEBUG:netmiko:Pattern is: screen\-length\ 0\ temporary
DEBUG:netmiko:_read_channel_expect read_data: screen-length 0 temporary
DEBUG:netmiko:Pattern found: screen\-length\ 0\ temporary screen-length 0 temporary
DEBUG:netmiko:screen-length 0 temporary
DEBUG:netmiko:Exiting disable_paging
DEBUG:netmiko:read_channel: Info: The configuration takes effect on the current user terminal interface only.
<netdevops>
DEBUG:netmiko:Clear buffer detects data in the channel
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:write_channel: b'\n'
DEBUG:netmiko:read_channel:
<netdevops>
DEBUG:netmiko:read_channel:
DEBUG:netmiko:[find_prompt()]: prompt is <netdevops>
DEBUG:netmiko:read_channel:
DEBUG:netmiko:write_channel: b'display version\n'
DEBUG:netmiko:Pattern is: display\ version
DEBUG:netmiko:_read_channel_expect read_data: display version
DEBUG:netmiko:Pattern found: display\ version display version
DEBUG:netmiko:read_channel: Huawei Versatile Routing Platform Software
VRP (R) software, Version 8.180 (CE12800 V200R005C10SPC607B607)
Copyright (C) 2012-2018 Huawei Technologies Co., Ltd.
HUAWEI CE12800 uptime is 0 day, 0 hour, 1 minute
SVRP Platform Version 1.0
<netdevops>
DEBUG:netmiko:write_channel: b'exit\n'
DEBUG:paramiko.transport:EOF in transport thread
Huawei Versatile Routing Platform Software
VRP (R) software, Version 8.180 (CE12800 V200R005C10SPC607B607)
Copyright (C) 2012-2018 Huawei Technologies Co., Ltd.
HUAWEI CE12800 uptime is 0 day, 0 hour, 1 minute
SVRP Platform Version 1.0
从控制台输出我们可以观察到,netmiko先通过paramiko去与设备建立连接通道。然后netmiko帮助我们发现提示符(prompt),取消分页。在执行命令之前,由于默认的auto_find_prompt为True,所以netmiko会再次自动发现提示符,方法是输入回车换行,确定提示符后发送命令。紧接着先去隧道中获取发送命令的回显,然后才去等待之前的提示符再次出现,这个时候代表回显结束。最后自动关闭到设备的连接。
这些debug信息都是netmiko代码里预埋的一些输出,当我们把日志的打印级别调整成debug的时候,netmiko预埋的debug及以上(info、warning、error)的相关信息都会在控制台输出。任何的问题,我们都会在debug信息中获取相关线索。
3.6.2 执行命令耗时较长导致send_command报错
大家在执行配置备份或者相关show信息的时候,经常会遇到程序报错:
OSError: Search pattern never detected in send_command: <netdevops>
在执行show信息的场景下出现pattern检测不到的,有两种情况,执行命令耗时较长,当前用户无取消分页的权限,在此我们讨论执行命令回显时间耗时较长这种情况。
部分命令存在执行时间特别长的情况,尤其是像display current-configuration这种查看配置类的,部分设备配置量巨大,回显耗时有时候会超过120秒,在初始化netmiko连接的时候,我们没有合理设置timeout参数,使用了默认值(100秒),在规定的时间内,netmiko一直读取隧道中的信息,且发现不了提示符,最终超时报错抛出异常,未找到指定正则。
这种情况我们只需要适当调整初始化连接的timeout参数,像笔者在实际生产中,根据回显时间最长设备的时间为基准,适当增加一点时间,将其设置为执行命令的超时时间timeout。比如笔者遇到过思科的nexus 5000交换机,挂满nexus 2000交换机,共有500左右端口,shwo interface执行时间大约2分半左右,所以笔者统一将timeout设置为180秒。有些场景需要我们取ping某个IP,如果网络状态不太好,也会极其消耗时间,导致执行超时。或者是执行一些下载介质的命令等等,类似的场景都可能有超时的风险,我们都可以适当调整timeout。
调整执行命令超时时间的示例:
import logging
from netmiko import ConnectHandler
logging.basicConfig(level=logging.DEBUG)
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'timeout':180,
'session_log': 'session.log',
}
with ConnectHandler(**dev) as conn:
output = conn.send_command('display version')
print(output)
只需要在设备连接信息中将timeout设置为比较大的值即可。
3.6.3 当前账户不具备取消分页的权限
有时候当前用户不具备取消分页的权限时,也会导致send_command报错,从错误堆栈来看与执行时间较长的情况没有差别,即使调整timeout也无济于事。这个时候我们可以看看debug信息,netmiko帮助我们执行取消分页命令的时,是否命令正确,是否权限足够,这些在log和debug信息中通过回显都可以观察到。其中绝大多数情况是,出于安全考虑。当前账户不具备执行取消分页命令的权限。所以在后面执行一些命令的时候,会卡在--more--
类似的回显上,需要我们输入回车才能继续回显。这个是由netmiko的机制决定的,出于各种考虑,netmiko的驱动基本在登录设备后都会自动取消分页,以便发送命令回显完整,不需要人为干预。这类原因导致的执行超时,我们可以在debug信息中看到类似如下的片段:
DEBUG:netmiko:Pattern found: display\ current\-configuration display current-configuration
DEBUG:netmiko:read_channel: !Software Version V200R005C10SPC607B607
!Last configuration was updated at 2022-07-22 00:49:26+00:00 by SYSTEM automatically
!Last configuration was saved at 2022-07-14 03:20:04+00:00 by netdevops
#
sysname netdevops
#
device board 1 board-type CE-MPUB
#
aaa
local-user netdevops password irreversible-cipher $1c$G$bj3b+5YM$v(1\!wt&S,lN.YU7XLnM)`5RAa{2eNj~J]CiH1iD$
local-user netdevops service-type ssh
local-user netdevops level 3
#
authentication-scheme default
#
authorization-scheme default
#
accounting-scheme default
#
domain default
#
domain default_admin
#
---- More ----
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
DEBUG:netmiko:read_channel:
...
DEBUG:netmiko:read_channel:
DEBUG:netmiko:write_channel: b'exit\n'
DEBUG:paramiko.transport:EOF in transport thread
Traceback (most recent call last):
...
File "C:\Pythons\python3.9\lib\site-packages\netmiko\base_connection.py", line 1535, in send_command
raise IOError(
OSError: Search pattern never detected in send_command: <netdevops>
从debug信息中我们可以观察到,回显卡在了分页的交互上面。
这种权限不够的只会影响回显0比较长的那种命令,对配置无影响。在我们仍坚持使用netmiko的前提下,一个是给用户权限进行调整,一个是自己写代码实现这种分页的交互,自己判断,自动输入回车。在条件允许的前提下,个人建议使用第一种方案,如果受制于各种因素,确实无法对用户权限调整,我们也可以通过一些netmiko的底层方法write_channel与read_channel为大家解决。
这两个方法分别用于在通信隧道(ssh或者telnet)发送命令和接收数据,二者一般搭配使用。之前讲的用于和设备交互的四个方法,在其最底层都是调用了这两个方法。
write_channel用于在通信隧道中发送命令,它只有一个参数out_data,用于发送给设备的命令,类型是字符串即可,它没有返回值。通过这个方法发送命令的时候我们一定要在后面手工追加换行符\n
(对于大多数设备是这样的,极个别设备由于其操作系统内核的相关设计,换行符可能是\r
、\r\n
)。实际netmiko在初始化的时候,在连接驱动类当中都设置好了默认的换行符——连接对象的RETURN属性。
read_channel用于在通信隧道中读取回显数据,这个方法没有参数,它每次只读取指定长度(65535字节)的字节,并使用utf8字符集解码返回,这个字符集目前无法指定,算是netmiko的一个bug。由于英文字符部分绝大多数字符集都是兼容的,所以一般不会出现乱码,这个问题不会暴露出来。但是对于一些国产操作系统而言,如果配置中出现中文,而国产操作系统可能是用gbk之类的字符集,这个bug就会触发,导致回显中会出现乱码。
基于这两个方法,我们对分页进行了处理,同时模仿send_command的pattern机制进行了主体逻辑的设计。
import re
import time
from netmiko import ConnectHandler
def send_command_for_uer_no_disable_paging(dev, cmd):
with ConnectHandler(**dev) as conn:
# 存放回显的变量
output = ''
# 判断回显结束的正则
output_end_pattern = r'<\S+>'
# 判断分页交互的正则
more_pattern = ' ---- More ----'
# 超时时间,隧道中读取内容的间隔时间,根据二者计算的循环次数
timeout = conn.timeout
loop_delay = 0.2
loops = timeout / loop_delay
i = 0
# 通过write_channel发送命令,命令后追加一个回车换行符
conn.write_channel('{}{}'.format(cmd, conn.RETURN))
# 进入循环,读取隧道中信息
while i <= loops:
# 读取隧道中的信息,放入chunk_output
chunk_output = conn.read_channel()
# 判断是否有分页交互提示
if more_pattern in chunk_output:
# 回显中的分页提示去除,去除一些影响回显展示的空格
chunk_output = chunk_output.replace(more_pattern, '').replace(' ','')
# 拼接回显
output += chunk_output
# 发送回车换行符
conn.write_channel(conn.RETURN)
# 根据提示符判断是否回显结束
elif re.search(output_end_pattern, chunk_output):
# 拼接回显 并跳出循环
output += chunk_output
break
# 停顿loop_delay秒
time.sleep(loop_delay)
# 计数器i加一 类似其他语言中的i++
i += 1
# 如果超过了,则证明超时,我们可以自己抛出异常
if i > loops:
raise Exception('执行命令"{}"超时'.format(cmd))
return output
if __name__ == '__main__':
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'session_log': 'session.log'
}
output = send_command_for_uer_no_disable_paging(dev, 'display current-configuration')
print(output)
我们与设备的整个交互就是通过write_channel将命令发送到设备,然后通过read_channel在通信隧道中读取回显,由于读取回显每次读取指定长度的,所以我们设计了一个while循环,每次读取让程序停顿一个间隔时间(loop_delay,取值0.2秒),读取到隧道中信息后我们判断是否有分页的一些正则,这块我们选取了字符串,更加简单一些,实际可以通过正则来处理。如果有分页则发送一个回车换行符。如果无正则,就判断是否有回显结束的提示符,有提示符就跳出循环,把拼接的回显返回给用户。如果最后循环结束,计数器肯定是大于循环次数的,这说明是程序超时未收到提示符。
3.6.4 如何netmiko去执行ping
我们在日常运维的时候,经常会有一些ping的检查,大家使用netmiko去做ping检查的时候,假如ping的包数比较多,包比较大,耗时比较长,会导致超时。即使不超时,由于netmiko的send_command机制,它直到回显全部返回才结束,而我们可能希望实时了解ping的情况。这种ping操作的场景,对时间和这种实时性有要求的时候,我们就可以尝试使用write_channel与read_channel,通过write_channel发送了ping的命令后,通过read_channel循环读取隧道中信息,实时将回显输出到某个地方,可以是直接标准输出到终端,也可以是写入某种MQ(消息队列),也可以是某种数据库(比如redis内存数据库或者传统的数据库)。同时可以自己加一些参数来控制超时时间等。
import time
from netmiko import ConnectHandler
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'session_log': 'session.log'
}
# 执行ping的相关变量
dest_ip = '192.168.137.1'
count = '100'
ping_cmd = 'ping -c {} {}'.format(count, dest_ip)
# 判断回显结束的标识
end_pattern = '>'
# 用于循环的相关变量
loop_delay = 0.2
timeout = 60
loops = timeout / loop_delay
i = 0
# 存放ping结果
ping_result = ''
with ConnectHandler(**dev) as conn:
# 发送ping命令
conn.write_channel('{}{}'.format(ping_cmd, conn.RETURN))
while i <= loops:
output = conn.read_channel()
if output:
# 此处我们进行打印,实际各个厂商的设备,
# ping的成功与否都会有对应符号特征,我们可以对一些情况就行计算或者解析实时输出到某处
print(output)
ping_result = ping_result + output
time.sleep(loop_delay)
i = i + 1
if end_pattern in output:
break
if i > loops:
raise Exception('ping超时')
print('ping 结果为:{}'.format(ping_result))
3.6.5 国产设备提权
关于提权之前的篇章也讲解过,我们可以通过send_command方法即可,注意调整expect_string和cmd_verify。
代码如下:
from netmiko import ConnectHandler
dev = {'device_type': 'cisco_ios',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'admin123!',
'port': 22,
'session_log': 'netdevops.log'
}
with ConnectHandler(**dev) as conn:
output = conn.send_command(command_string='enable', expect_string='assword:')
print(output)
output = conn.send_command(command_string='admin1234!', expect_string='#')
print(output)
这里我们再使用write_channel和read_channel来给大家实现一遍,提供一种不同的思路。
from netmiko import ConnectHandler
import time
dev = {'device_type': 'cisco_ios',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'admin123!',
'secret': 'admin1234!',
'port': 22,
'session_log': 'netdevops.log'
}
# 提权的命令
enable_cmd = 'enable'
# 提权时输入密码的标识
password_pattern = 'assword'
# 提权成功后的标识
enable_success_pattern = '#'
with ConnectHandler(**dev) as conn:
# 通过通信隧道发送提权命令
conn.write_channel('{}{}'.format(enable_cmd, conn.RETURN))
# 让程序等待2秒,如果不停顿,直接读取隧道,隧道中可能还来不及接收任何数据
time.sleep(2)
# 停顿2秒后读取数据
output = conn.read_channel()
# 确认回显中有输入密码的提示
if password_pattern not in output:
raise Exception('请确认输入的提权命令正确,未发现输入密码的提示')
# 输入密码
conn.write_channel('{}{}'.format(dev['secret'], conn.RETURN))
# 停顿并读取数据
time.sleep(2)
output = conn.read_channel()
# 根据回显判断判断是否成功
if enable_success_pattern in output:
print('提权成功')
elif password_pattern not in output:
raise Exception('提权密码错误,请确认相关信息')
else:
raise Exception('发生未知错误,提权失败')
在这段代码中,我们通过write_channel与read_channel这两个偏底层的方法与网络设备交互,在发送命令的时候要注意加回车换行符,在读取数据前要注意让程序停顿一段时间,文中给出了2秒,大家可以根据网络延迟和设备性能合理优化这个时间。同时为了让程序更健壮,我们对回显进行了判断,对一些异常情况进行了识别。
在与设备的相关交互中,利用write_channel与read_channel这种底层的方法,理论上可以实现任何的场景。我们可以把一些比较常用的场景进行固化,如果这个场景使用send_command等四个方法有所局限,我们可以转而使用这两个方法。
3.6.6 使用send_command_timing 回显不全
在我们的一些配置备份场景下,我们假如使用了send_command_timing经常会遇到配置不全的情况,产生这种问题有两种原因:
- 执行命令耗时较长,在timeout规定的时间窗口内未收集全配置,send_command_timing不再读取隧道中信息,强制退出,将已收集到的回显返回。
- 设备在传输回显的过程中,会稍微“卡顿”几秒,比如思科的设备在查看配置的时候,会在building configuration回显处卡住几秒。。而这时,默认判断回显为空的时间间隔比较小(默认值是2秒),netmiko判定这个时间窗口内无数据回显,回显已经结束。
我们的整体解决方案是注意创建连接时的timeout,将其设置为一个稍微长点的合理值,同时在调用send_command_timing方法时,传入一个delay_factor参数,将延迟因子适当调大,延迟因子默认为1,延迟基数是2秒,所以判断回显为空的默认时间为二者乘积2秒,我们每调大一下延迟因子,判定为空的时间窗口就会2秒的倍数级增加。大家可以根据实际情况,判定一下比较合适的延迟因子。
关于延迟因子实际有一个内部的判断机制。相关内容在登录慢、执行慢章节内容详细讲解。
从代码上以上调整如下:
from netmiko import ConnectHandler
# 设备连接信息 timeout调大
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'timeout': 180,
'session_log': 'session.log',
}
with ConnectHandler(**dev) as conn:
# send_command_timing中调大延迟因子delay_factor(其默认值为1)
output = conn.send_command_timing('display current-configuration', delay_factor=3)
print(output)
3.6.7 登录与执行速度比较慢
netmiko和设备交互,都是通过write_channel写入命令到通信隧道中,然后以某种频率去隧道中循环读取数据。所以在网络条件恒定的前提之下,适当调小停顿等待时间,进而调高隧道中读取的频率,可以让回显尽早收集齐,完成相关操作的确认。而提高整个频率就是靠延迟因子的调整,我们之前讲解的延迟因子很多都是方法内部的延迟因子,实际上netmiko的延迟因子不是只有这一个,或者说,这个延迟因子只是局部延迟因子(大部分局部的延迟因子默认值都是1),还有一个隐藏的全局延迟因子。只要是和设备有相关命令行交互的方法,大都会有delay_factor这个参数。但是并不是由这个局部延迟因子单独决定最终的实际延迟因子进而控制频率的,netmiko有一套比较灵活的延迟因子选举机制:
在初始化连接的时候,有一个全局延迟因子global_delay_factor(默认值为1)和fast_cli模式(默认值为False)。
当不开启fast_cli模式的时候,netmiko在各个方法内选择局部延迟因子delay_factor和全局延迟因子global_delay_factor中的较大者。所以我们单纯调整任何一个延迟因子不得其法可能都不会有效果。
当开启fast_cli模式的时候,netmiko会认为用户需要优化读取数据的性能,需要提高读取信息的频率,这时它会选择局部延迟因子delay_factor和全局延迟因子global_delay_factor中的较小者。
以上行为是netmiko内部的select_delay_factor方法的基本逻辑,各个方法执行的时候基本都先调用此方法来判定最终的延迟因子,比如取消分页的时候,在取消分页方法内部调用select_delay_factor方法,大多数设备给其赋值局部延迟因子是1,这个时候我们单纯调整全局的延迟因子,并不会提高效率。
为了调高读取隧道信息中的频率,我们调小程序停顿等待的时间,调小延迟因子是我们的“出路”。结合上面的逻辑,为了提高登录速度,fast_cli调整为True,开启快速模式,全局延迟因子global_delay_factor调小,比如笔者设置为0.1。有些设备不调整参数,使用默认值,笔者的模拟器设备华为CE交换机登录耗时大约8秒甚至更久,但是调整上面的参数后,时间可以缩短到一秒。
from netmiko import ConnectHandler
import time
# 设备连接信息 开启fast_cli快速模式,全局global_delay_factor延迟因子调小(默认值为1)
dev = {'device_type': 'huawei',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'timeout': 180,
'fast_cli':True,
'global_delay_factor':0.01,
'session_log': 'session.log',
}
print(time.time())
with ConnectHandler(**dev) as conn:
print(time.time())
print(conn.is_alive())
以上方法实际上也会让执行速率变快,但是这块有时候在感官上并不是特别明显。全局延迟因子已经调整的比较小的情况下,各类发送命令或者配置的延迟因子也可以无需调整。
3.6.8 不知道自己该选择哪个device_type
在Netmiko章节之初我们讲过它支持众多的设备类型,也罗列出了一部分常见的device_type。但是Netmiko支持的平台众多,我们写给大家一段代码,它可以输出当前版本netmiko所支持的所有平台。
from netmiko import platforms
# 列表推导式,把所有的ssh驱动取出
ssh_platforms = [i for i in platforms if 'telnet' not in i and 'serial' not in i and 'ssh' not in i]
# 去除一些厂商平台的device_type
if 'abc' in ssh_platforms:
ssh_platforms.remove('abc')
if 'autodetect' in ssh_platforms:
ssh_platforms.remove('autodetect')
if 'terminal_server' in ssh_platforms:
ssh_platforms.remove('terminal_server')
# 列表推导式取出telnet驱动
telnet_platforms = [i for i in platforms if 'telnet' in i]
# 列表推导式取出serial驱动
serial_platforms = [i for i in platforms if 'serial' in i]
我们通过导入platforms这一内部变量,从字符串的一个in计算判断其协议类型,整理输出。
ssh驱动有以下: ['a10', 'accedian', 'adtran_os', 'alcatel_aos', 'alcatel_sros', 'allied_telesis_awplus', 'apresia_aeos', 'arista_eos', 'aruba_os', 'aruba_osswitch', 'aruba_procurve', 'avaya_ers', 'avaya_vsp', 'broadcom_icos', 'brocade_fastiron', 'brocade_fos', 'brocade_netiron', 'brocade_nos', 'brocade_vdx', 'brocade_vyos', 'calix_b6', 'cdot_cros', 'centec_os', 'checkpoint_gaia', 'ciena_saos', 'cisco_asa', 'cisco_ftd', 'cisco_ios', 'cisco_nxos', 'cisco_s300', 'cisco_tp', 'cisco_wlc', 'cisco_xe', 'cisco_xr', 'cloudgenix_ion', 'coriant', 'dell_dnos9', 'dell_force10', 'dell_isilon', 'dell_os10', 'dell_os6', 'dell_os9', 'dell_powerconnect', 'dlink_ds', 'eltex', 'eltex_esr', 'endace', 'enterasys', 'ericsson_ipos', 'extreme', 'extreme_ers', 'extreme_exos', 'extreme_netiron', 'extreme_nos', 'extreme_slx', 'extreme_vdx', 'extreme_vsp', 'extreme_wing', 'f5_linux', 'f5_ltm', 'f5_tmsh', 'flexvnf', 'fortinet', 'generic', 'generic_termserver', 'hp_comware', 'hp_procurve', 'huawei', 'huawei_olt', 'huawei_smartax', 'huawei_vrpv8', 'ipinfusion_ocnos', 'juniper', 'juniper_junos', 'juniper_screenos', 'keymile', 'keymile_nos', 'linux', 'mellanox', 'mellanox_mlnxos', 'mikrotik_routeros', 'mikrotik_switchos', 'mrv_lx', 'mrv_optiswitch', 'netapp_cdot', 'netgear_prosafe', 'netscaler', 'nokia_sros', 'oneaccess_oneos', 'ovs_linux', 'paloalto_panos', 'pluribus', 'quanta_mesh', 'rad_etx', 'raisecom_roap', 'ruckus_fastiron', 'ruijie_os', 'sixwind_os', 'sophos_sfos', 'supermicro_smis', 'tplink_jetstream', 'ubiquiti_edge', 'ubiquiti_edgerouter', 'ubiquiti_edgeswitch', 'ubiquiti_unifiswitch', 'vyatta_vyos', 'vyos', 'watchguard_fireware', 'yamaha', 'zte_zxros']
ssh共有驱动:110
telnet驱动有以下: ['adtran_os_telnet', 'apresia_aeos_telnet', 'arista_eos_telnet', 'aruba_procurve_telnet', 'brocade_fastiron_telnet', 'brocade_netiron_telnet', 'calix_b6_telnet', 'centec_os_telnet', 'ciena_saos_telnet', 'cisco_ios_telnet', 'cisco_xr_telnet', 'dell_dnos6_telnet', 'dell_powerconnect_telnet', 'dlink_ds_telnet', 'extreme_exos_telnet', 'extreme_netiron_telnet', 'extreme_telnet', 'generic_telnet', 'generic_termserver_telnet', 'hp_comware_telnet', 'hp_procurve_telnet', 'huawei_olt_telnet', 'huawei_telnet', 'ipinfusion_ocnos_telnet', 'juniper_junos_telnet', 'nokia_sros_telnet', 'oneaccess_oneos_telnet', 'paloalto_panos_telnet', 'rad_etx_telnet', 'raisecom_telnet', 'ruckus_fastiron_telnet', 'ruijie_os_telnet', 'supermicro_smis_telnet', 'tplink_jetstream_telnet', 'yamaha_telnet', 'zte_zxros_telnet']
telnet共有驱动:36
serial驱动有以下: ['cisco_ios_serial']
serial共有驱动:1
我们说过如何赋值device_type是一个经验活儿。但是在我们日常使用中,可能情况会比较复杂,对于新手不清楚自己手中的设备应该选择哪个device_type,或者设备众多,单靠经验一台台判断,成本比较高。这个时候我们可以调用netmiko的device_type自动检测机制,其代码示例如下:
from netmiko import SSHDetect, Netmiko, ConnectHandler
# Netmiko等同于ConnectHanler
import logging
logging.basicConfig(
level=logging.DEBUG,
)
dev = {
'device_type': 'autodetect',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'port': 22,
'session_log':'session.log'
}
# 创建一个Detect的对象,将参数赋值
guesser = SSHDetect(**dev)
# 调用autodetect ,进行device_type的自动判断,返回结果是一个最佳结果的字符串,这个字符串就是netmiko自动判断的device_type
# 无法判断的时候返回的是None
best_match = guesser.autodetect()
print('best_match is:{}'.format(best_match))
其输出结果为:
best_match is:huawei
这段代码我们创建一个netmiko平台检查工具类SSHDetect的实例,初始化的参数只需要把device_type赋值为autodetect
即可,然后调用这个检查对象的autodetect方法,它会映射到内部一个通用的终端驱动类,device_type值为terminal_server,对应的驱动类为TerminalServerSSH。这个类实例化后的连接对象,登陆到设备后,不会进行相关准备工作(比如取消分页),只会创建最原始的通信隧道。netmiko内部记录了近30余平台的命令及回显检查机制,当我们调用autodetect方法时,它会一个个尝试这近30组规则,先输入一个指定的命令,大部分是show version、display version类似的命令,如果回显中有指定的字符(通过一组正则去判断)则确定当前设备的device_type,比如有“Huawei Technologies”则认为当前device_type应该是huawei。如果当前规则不匹配则执行下一条,假如到最后也匹配不上一组规则,则此方法返回值为None。
对于一些主流的网络设备,此平台可以帮助大家提供一些简洁的方法给出device_type的最佳推荐值。大家在检测完之后建议将结果保存到某处,进行微调后以便后续再有此设备的时候以保存的结果为依据即可。
3.6.9 回显有中文配置时乱码
我们在执行之前的配置备份脚本时,会遇到一些国产设备的配置为乱码的情况,这种问题主要是因为配置中存在中文的原因。netmiko底层在发送命令从隧道中读取回显,这时的数据都是字节流,netmiko会强制以utf8字符集解码保存,而很多国产设备字符集可能使用的非UTF8,在用英文的配置时,这些字符集是兼容的,但当使用中文的时候字符集不兼容,而netmiko 3.4.0及以前版本(即使是4.X的版本也存在,笔者已经提交过issue,netmiko作者已经修改,目前在还没稳定版发布)会强制使用UTF8解码,最终导致中文部分的配置会出现乱码。
这个问题如果要解决,有两种途径,一种是修改配置为英文。另外一种就是要修改代码,修改部分比较多,对新手并不是很友好,不推荐。
Netmiko3.4.0版本读取配置时的字符集无法控制,但是推送配置时的命令如果存在中文,是可以进行控制的,需在初始化连接的时候赋值encoding参数为对应的字符集,这样发送的命令才会以指定的字符集编码,发送给网络设备。这个参数的赋值同时会影响session_log日志保存时的编码,这点需要大家注意。
3.6.10 没有驱动的网络设备该如何处理
网络设备型号一直众多,Netmiko虽然集成了110个驱动,但是在做网络自动化的过程中,我们总是会遇到一些比较“小众”的型号,或者遇到因软件版本的原因导致的不兼容问题。这个时候我们如何处理呢?一种思路是使用paramiko之类的SSH工具包,另外一种其实就是Netmiko的隐藏的device_type值terminal_server,对应一个隐藏的驱动类TerminalServerSSH,从名字上来看,大家就会发现它是一个比较通用的终端驱动。
它只直接继承自BaseConnection这个最原始的连接类,它登录到设备,不会进行取消分页等类似的操作。它只负责登录到设备,创建一个通信隧道。有点类似于paramiko,但是相关参数又比paramiko简单一些。在创建完通信隧道后,我们的“玩法”就比较多了:
方法1:我们可以只调用write_channel与read_channel方法进行原始通信,发送命令接收回显,针对一些比较短的回显,比如show version,或者单行刷配置(刷配置的回显非常短),我们都可以直接按这种思路进行。
方法2:我们也可以把一些比较个性化的部分通过这两个方法执行,比如取消分页、提权等,之后的继续使用send_command或者send_config_set等方法即可。
方法3:我们调用write_channel与read_channel方法,再结合send_command的基本思路进行封装,针对日常的show命令和配置命令进行相关个性化编写。
针对方法1,示例如下
from netmiko import ConnectHandler
import time
dev = {'device_type': 'terminal_server',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'secret': 'Admin1234~',
'port': 22,
'session_log': 'netdevops.log'
}
RETURN = '\n'
with ConnectHandler(**dev) as conn:
# 通过通信隧道发送提权命令
conn.write_channel('show version{}'.format(RETURN))
# 让程序等待2秒,如果不停顿,直接读取隧道,隧道中可能还来不及接收任何数据
time.sleep(5)
output = conn.read_channel()
print(output)
我们只需要对设备发送命令,停顿,确保隧道中有回显,由于此类回显非常低,我们可以直接读取,无需循环读取判断结束。但是类似display current-configuration回显比较多的情况下,上述代码可能存在回显不全的情况,我们可以按照send_command的思路进行一个循环读取并判断是否回显结束的逻辑
from netmiko import ConnectHandler
import time
import re
dev = {'device_type': 'terminal_server',
'host': '192.168.137.201',
'username': 'netdevops',
'password': 'Admin123~',
'secret': 'Admin1234~',
'port': 22,
'session_log': 'netdevops.log'
}
more_pattern = ' ---- More ----'
RETURN = '\n'
timeout = dev.get('timeout', 100)
loop_delay = 0.2
loops = timeout / loop_delay
i = 0
cmd = 'display version'
# 有些设备登录后会有提示修改密码的交互,才进入banner
# 我们可以修改配置关闭此类配置,也可以对其判断,输入否定选项
change_password_pattern = '[Y/N]:'
with ConnectHandler(**dev) as conn:
# 模拟人工多输入几个回车,遇到一些修改密码的
# 只有发送信息给网络设备,才能有新的回显
conn.write_channel(RETURN)
time.sleep(1)
output = conn.read_channel()
# 如果有提示密码修改的交互,则输入否(N)
if change_password_pattern in output:
conn.write_channel('N{}'.format(RETURN))
time.sleep(1)
output = conn.read_channel()
# 在show情况下,最后一行就是提示符,作为回显结束的判断依据
prompt_pattern = output.splitlines()[-1]
print('当前提示符为:{}'.format(prompt_pattern))
# 重新初始化回显,置空
output = ''
# 通过通信隧道发送提权命令
conn.write_channel('{}{}'.format(cmd, RETURN))
while i <= loops:
# 读取隧道中的信息,放入chunk_output
chunk_output = conn.read_channel()
# 判断是否有分页交互提示
if more_pattern in chunk_output:
# 回显中的分页提示去除
chunk_output = chunk_output.replace(more_pattern, '')
# 拼接回显
output += chunk_output
# 发送回车换行符
conn.write_channel(RETURN)
# 根据提示符判断是否回显结束
elif re.search(prompt_pattern, chunk_output):
# 拼接回显 并跳出循环
output += chunk_output
break
# 停顿loop_delay秒
time.sleep(loop_delay)
# 计数器i加一 类似其他语言中的i++
i += 1
# 如果超过了,则证明超时,我们可以自己抛出异常
if i > loops:
raise Exception('执行命令"{}"超时'.format(cmd))
print(output)
这段代码,我们结合了之前取消分页权限不足的相关逻辑。只需要修改一些变量,能达到比较通用的效果。但是在实际使用中还是要结合场景及交互可能出现的情况,甚至是意外情况等在合适的位置对回显进行相关的判断与确认。上述回显的时候,会把我们输入的命令也输出出来,如果在做配置备份等场景,想去掉这个发送的命令,我们可以在前面读取的时候用空字符串替换掉。
这段代码也可以进一步封装成函数,方便我们重复使用。