《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。

NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。

由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注

本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注

第三章 基于Netmiko的网络设备交互

3.1 Netmiko简介及安装

网工与网络设备最习惯的交互方式就是CLI,通过SSH或者Telnet登录到设备,执行命令,实现信息的获取和配置的推送,是网工最熟悉的工作方式。

所以基于CLI的网络自动化开发,也成为了网工入门NetDevOps的必经之路与最优之解。

Python基于CLI模式与网络设备进行交互的工具包有很多,而netmiko与paramiko无疑是这类工具包中的两颗璀璨明珠。

本章节我们将介绍一下netmiko,它是工程师Kirk Byers 于2015年发布的一个用于简化众多厂商网络设备CLI连接(ssh、telnet、serial)的工具类。实际2015年的版本只支持ssh,所以它最早是为了简化ssh通用工具类paramiko的连接和操作(后续netmiko已经支持了telnet和serial),所以它的名字从中取了"miko",又因其垂直于网络自动化领域,所以又添加了“net”,组成了netmiko的名称。

相较paramiko,netmiko有着更加简化的操作和更加针对网络设备的高阶封装。通过抽象一些底层的状态控制,消除查看配置和改变配置的在一众平台中的差异,目前支持的厂商平台覆盖了日常网络运维所需,所以笔者更建议新手使用netmiko,让我们更聚焦在网络业务逻辑层面,这些我们也会在后续的使用中都将体会到。

经过7年的迭代,netmiko进入到了4.0时代,增加了很多非常优秀的方法,支持非常灵活的参数配置。

由于netmiko4.0近期发布,针对国产设备有一些小的bug(比如执行成功,但是不返回回显),同时考虑到4.0版本添加的参数和方法对对新手而言使用成本偏高,且上个版本也可以通过其他方法达到类似效果,所以笔者选择比较稳定、使用比较广泛的3.4.0版本来进行netmiko的相关介绍,本章节介绍的netmiko3.4.0的主要方法在4.0及以上版本是兼容的。

我们先使用pip进行netmiko的安装,并指定版本3.4.0,指定版本的方式是在包名后面加上两个等号后接版本号。

 pip install netmiko==3.4.0

我们打开一个交互式的窗口,或者编写一个脚本,导入netmiko的对应函数没有报错,代表安装成功。

from netmiko import Netmiko

3.2 支持的网络设备平台

netmiko针对主流的网络设备平台进行了适配,编写了对应的驱动类,这些驱动类提供了很多便捷的方法,比如登录的时候帮我们检查一下是否登录完成,自动帮助我们输入用户名密码,自动帮我们取消分页,执行命令的时候自动帮我们判断回显是否结束,从文件中读取配置等等。

这些就是驱动类的作用,netmiko经过了7年的发展和沉淀从最初的不到10个驱动类,到今天已经壮大到了110个驱动类。适配了市面上主流的网络设备平台,且其高度的封装,也可以让用户比较方便的编写出自己的驱动类。

Netmiko支持的110个驱动类主要分为三大类:

Regularly tested(定期测试)

每次netmiko发版都会对相关设备平台进行完整的测试,在netmiko中有完整的测试脚本。

这些平台有Arista vEOS、Cisco ASA、Cisco IOS、Cisco IOS、XE、Cisco IOS、XR、Cisco NX、OS、Cisco SG300、HP ProCurve、Juniper Junos、Linux。这里面的设备以国外的平台为主,思科居多。

Limited testing(有限测试)

不进行完整的测试,在某些时间点对特定配置与操作回显进行测试。作为开发者没有那么多的网络设备用于测试,所以在适配后,仅在某些时间点(特定版本)进行基础功能的测试。这些平台有6Wind、Adtran OS、Alcatel AOS6/AOS8、Apresia Systems AEOS、Broadcom ICOS、Calix B6、Centec Networks、Cisco AireOS (Wireless LAN Controllers)、CloudGenix ION、Dell OS9 (Force10)、Dell OS10、Dell PowerConnect、Ericsson IPOS、Extreme ERS (Avaya)、Extreme VSP (Avaya)、Extreme VDX (Brocade)、Extreme MLX/NetIron (Brocade/Foundry)、HPE Comware7、Huawei、Huawei OLT、Huawei SmartAX、IP Infusion OcNOS、Juniper ScreenOS、Mellanox、MikroTik RouterOS、MikroTik SwitchOS、NetApp cDOT、Netgear ProSafe、Nokia/Alcatel SR OS、OneAccess、Palo Alto PAN、OS、Pluribus、Ruckus ICX/FastIron、Ruijie Networks、Supermicro SMIS、TPLink JetStream、Ubiquiti EdgeSwitch、Vyatta VyOS、Yamaha、ZTE ZXROS

有限测试的设备平台中,会出现华为、华三、锐捷等主流国产品牌的众多系列,根据笔者在日常使用中的情况来看,基本满足配置的采集和推送,复杂一些的需求也可以通过netmiko提供的灵活的参数来实现。

Experimental(实验性质)

实验性能意味着只是对设备进行了基础的适配,没有相应的单元测试。这些设备平台有A10、Accedian、Allied Telesis AlliedWare Plus、Aruba、Brocade Fabric OS、C-DOT CROS、Ciena SAOS、Citrix Netscaler、Cisco Telepresence、Check Point GAiA、Coriant、Dell OS6、Dell EMC Isilon、Eltex、Enterasys、Endace、Extreme EXOS、Extreme Wing、Extreme SLX (Brocade)、F5 TMSH、F5 Linux、Fortinet、MRV Communications OptiSwitch、MRV LX、Nokia/Alcatel SR-OS、QuantaMesh、Rad ETX、Raisecom ROAP、Sophos SFOS、Ubiquiti Unifi Switch、Versa Networks FlexVNF、Watchguard Firebox、6WIND TurboRouter。

这类设备比较常见的可能是F5的两种模式的驱动。笔者日常接触不是很多,主要用于配置的信息收集也是问题不大。

以上就是netmiko支持的设备类型,覆盖了足足有110大类的驱动。基本覆盖了网工日常使用的常见网络设备,netmiko支持telnet、ssh协议的CLI,但是部分设备的telnet是不支持的,支持telnet的设备平台是36种,但也覆盖了思科、华为、华三、Juniper的主要设备平台。同时考虑到出于安全考虑,大部分网络设备已经以ssh协议为主,所以本章节讲解的CLI以ssh协议为主。对于不支持的,笔者后续也会补充一些paramiko和telnlib的内容。

3.3 创建连接

我们在对设备进行操作之前首先要登录到设备,这个时候我们需要借助于一个函数ConnectHandler,从命名风格而言是大驼峰命名法(单词间通过大小写区分,每个单词首字母大写,长得像驼峰),但它实打实的是一个函数,它返回的是到网络设备的一个长连接对象。

关于ConnectHandler函数的参数分为device_type和其他连接驱动类实例化所需参数。

device_type即我们之前所说的设备平台也可以翻译为设备类型,是由字符串表示。

连接驱动类实例化所需参数主要包括创建ssh连接所需的设备IP、用户名、密码、连接端口号等等。

代码示例如下:

from netmiko import ConnectHandlerconn = ConnectHandler(device_type='cisco_ios',host='192.168.137.201',username='netdevops',password='admin123!',port=22)print(conn)
'''以上输出一个实际的连接对象
<netmiko.cisco.cisco_ios.CiscoIosSSH object at 0x000002DBC9F23F10>
'''
conn.disconnect()

这个代码相较于其他的ssh工具更符合网工的思维模式,在传参的时候,如果参数过多,挤在一行会影响可读性,通过适当的换行,对齐参数,可以有效提高代码可读性,我们使用了哪些参数,赋值了哪些值,都一目了然。

这段代码仍有优化的空间,由于是打开一个到设备的长连接,为了节省资源,我们应该在程序结束时关闭连接,这个时候又可以使用with上下文管理了,我们习惯将打开的连接通过as起一个别名conn,这个conn就是到这个网络设备的一个连接

with上下文管理器的本质是在创建对象后调用了对象的__enter__方法,代码块结束的时候自动帮助我们调用with管理的对象的__exit__方法,只要实现了此方法的类创建的对象,均可以使用with进行上下文管理。netmiko就实现了这两个方法,在使用with上下文管理创建netmiko连接对象后,调用__enter__方法将连接对象"自己"(回想在类与对象章节中的self)返回,在离开with上下文管理后,它对象的__exit__方法,这个方法又调用了“自己”的disconnect方法,实现了断开连接的效果。示例:

from netmiko import ConnectHandlerwith ConnectHandler(device_type='cisco_ios',host='192.168.137.201',username='netdevops',password='admin123!',port=22) as conn:print(conn)print(conn.is_alive())
'''以上代码会输出
<netmiko.cisco.cisco_ios.CiscoIosSSH object at 0x000002570EB84E50>
False
'''

我们通过is_alive这个方法进行了判断,结果显示连接已经断开,这可以有效防止因为程序导致的过多用户登录设备而未释放连接而引发其他问题。

关于这段参数我们还可以在调用ConnectHandler时将参数的赋值方式进行优化,其基本方法是先定义一个字典,包含了ConnectHandler所需的参数,这些参数作为key,然后value是我们期望赋的值,调用ConnectHandler函数的时候,将这个字典前加双星号**传入,等同于上面脚本的效果。示例:

from netmiko import ConnectHandlerdev = {'device_type': 'cisco_ios','host': '192.168.137.201','username': 'netdevops','password': 'admin123!','port': 22}with ConnectHandler(**dev) as conn:print(conn)
'''以上代码会输出
<netmiko.cisco.cisco_ios.CiscoIosSSH object at 0x000002570EB84E50>
'''

这种双星展开赋值的方式,可以使代码的可读性进一步提高,参数赋值变的更简单。

“连接”背后的逻辑

那netmiko在ConnectHandler函数中究竟做了什么,让它可以创建众多设备的网络连接呢?

这段内容仅试图讲清楚netmiko背后的逻辑,方便我们在某些特定场合进行debug,或者是编写自己的驱动类,对于普通用户如果觉得过于费解,也可以直接跳过,或等能力足够的时候再回过头来看看。

示意图:

用户创建连接网络设备传入众多参数,其中device_type用于判断该使用哪个驱动类(netmiko的110个驱动类)。

# netmiko/ssh_dispatcher.py
def ConnectHandler(*args, **kwargs):"""Factory function selects the proper class and creates object based on device_type."""device_type = kwargs["device_type"]if device_type not in platforms:if device_type is None:msg_str = platforms_strelse:msg_str = telnet_platforms_str if "telnet" in device_type else platforms_strraise ValueError("Unsupported 'device_type' ""currently supported platforms are: {}".format(msg_str))# 根据device_type获取连接类ConnectionClass = ssh_dispatcher(device_type)# 透传所有参数,实例化连接类return ConnectionClass(*args, **kwargs)

在ssh_dispatcher函数中,有一个关键的名为CLASS_MAPPER的字典,将各类名称的字符串映射到了各个驱动类(我们之前在字典部分说过,dict的值可以是任意数据结构,包括类,甚至是方法)。

# netmiko/ssh_dispatcher.py
def ssh_dispatcher(device_type):# 根据传入的设备类型映射到对应的连接类并返回"""Select the class to be instantiated based on vendor/platform."""return CLASS_MAPPER[device_type]

CLASS_MAPPER是经过一系列加工,最终形如下的一个字典:

CLASS_MAPPER = {"cisco_asa": CiscoAsaSSH,"cisco_ftd": CiscoFtdSSH,"cisco_ios": CiscoIosSSH,"cisco_nxos": CiscoNxosSSH,"cisco_wlc": CiscoWlcSSH,"cisco_xe": CiscoIosSSH,"cisco_xr": CiscoXrSSH,"f5_ltm": F5TmshSSH,"f5_tmsh": F5TmshSSH,"f5_linux": F5LinuxSSH,"fortinet": FortinetSSH,"hp_comware": HPComwareSSH,"hp_procurve": HPProcurveSSH,"huawei": HuaweiSSH,"huawei_smartax": HuaweiSmartAXSSH,"huawei_olt": HuaweiSmartAXSSH,"huawei_vrpv8": HuaweiVrpv8SSH,"juniper": JuniperSSH,"juniper_junos": JuniperSSH,"juniper_screenos": JuniperScreenOsSSH,"linux": LinuxSSH,"ovs_linux": OvsLinuxSSH,"ruijie_os": RuijieOSSSH,"vyatta_vyos": VyOSSSH,"vyos": VyOSSSH,"zte_zxros": ZteZxrosSSH,
}

每个成员的value都是一个厂商系列的连接驱动类。然后netmiko将实例化连接所需的参数再透传给了具体的驱动类,实际device_type也会被传给驱动类,但是主要是用于判断使用ssh还是telnet协议,暂时不在我们讨论范围,先跳过。

各种驱动类通过构造函数(初始化方法)来进行一系列登录设备所需的操作及登录后的一些操作,这些操作调用了paramiko打开了一个ssh隧道,然后输入用户名密码实现登录,之后netmiko还会帮助我们做一些事情,比如读取banner,执行取消分页的操作,判断登录之后的提示符(一般多为设备名加#,>,]等),这些操作在不同的驱动中大同小异,会稍微有出入。执行完这些操作后,连接就创建了,返回了一个连接类对象。

如何赋值device_type?

实例化的时候,我们的device_type该如何赋值以便能使用准确的驱动呢?这其实是一个经验活儿。

首先关于device_type可以取的值,我们需要去看CLASS_MAPPER。CLASS_MAPPER实质为一个字典,key为device_type,value为对应的Connection驱动类,在这个文件中记录了各个device_type所对应的驱动类。限于篇幅做了上述代码中的映射关系删减,保留了大家可能遇到的。

比如笔者常用的是huawei、cisco_ios等。通过字面我们也可以大体判断出每个平台使用的哪个驱动,比如思科的nexus系列设备应该使用cisco_nxos作为device_type的值,而华为的大部分设备如CE系列交换机使用huawei作为device_type,Juniper的junos系列可以使用juniper,锐捷的使用ruijie_os作为device_type,诸如此类。同时我们也有一个netmiko的方法可以判断出此设备netmiko是否支持,该使用哪种驱动,这种方法我们在后续的netmiko的“疑难杂症”篇进行讲解。

如果相对应的厂商型号netmiko支持telnet协议,则在对应的device_type值后添加_telnet即可,比如华为CE交换机的驱动是“huawei”(当然我们也可以写作“huawei_ssh”),默认是走SSH协议,在device_type值后面我们可以追加协议类型,比如使用telnet,则device_type值应为“huawei_telnet”,netmiko则会调用对应的telnet连接类。

连接类初始化参数详解

创建连接的示例中我们还有很多其他参数,他们实际是用于创建连接的关键参数,对应了各个驱动类的__init__方法中的参数。

我们先看一段代码

# netmiko/base_connection.py
class BaseConnection(object):"""Defines vendor independent methods.Otherwise method left as a stub method."""def __init__(self,ip="",host="",username="",password=None,secret="",port=None,device_type="",verbose=False,global_delay_factor=1,global_cmd_verify=None,use_keys=False,key_file=None,pkey=None,passphrase=None,allow_agent=False,ssh_strict=False,system_host_keys=False,alt_host_keys=False,alt_key_file="",ssh_config_file=None,## Connect timeouts# ssh-connect --> TCP conn (conn_timeout) --> SSH-banner (banner_timeout)#       --> Auth response (auth_timeout)conn_timeout=5,auth_timeout=None,  # Timeout to wait for authentication responsebanner_timeout=15,  # Timeout to wait for the banner to be presented (post TCP-connect)# Other timeoutsblocking_timeout=20,  # Read blocking timeouttimeout=100,  # TCP connect timeout | overloaded to read-loop timeoutsession_timeout=60,  # Used for locking/sharing the connectionkeepalive=0,default_enter=None,response_return=None,serial_settings=None,fast_cli=False,_legacy_mode=True,session_log=None,session_log_record_writes=False,session_log_file_mode="write",allow_auto_change=False,encoding="ascii",sock=None,auto_connect=True,):...

这个类就是netmiko的所有连接类的基类(可以理解为这个netmiko连接家族的老祖宗),它的构造方法中有着众多的参数,用于去创建一个连接对象,即登录到一台设备。笔者根据实际使用,认为需要重点关注的是以下参数:

说明 参数名
ip 设备IP地址
host 设备host名称,需要可以解析出IP地址
username 用户名
password 密码
secret enable密码,提权时需要输入的密码
port 端口,会根据device_type自动判断(如有“ssh”字符串是22,如有“telnet”是23),也可以用户指定
device_type 设备的驱动名称
conn_timeout 连接超时时间,默认为5秒,如果网络条件不好,建议适当增加,尤其是广域网或者国际网络
timeout 执行命令的超时时间,根据情况适当延长,默认为100秒
session_log 记录log的路径,如填写则会将与设备的所有操作记录
session_log_file_mode 记录log的模式,覆盖"write" ,追加 “append”,默认覆盖
global_delay_factor 默认的全局的延迟因子,值为1,结合后续篇章了解,在这里引出,在某些场景需要它参与计算,但是一般不修改。
fast_cli 快速模式,默认False,不开启,netmiko内部提供一种优化性能的方式,设置为True后,在与设备交互过程中读取隧道中信息的频率会加快(全局延迟因子与单个方法中的延迟因子取较小值)。

ip和host两个值最终会赋值给连接对象的host属性(self.host),ip或者host尽量写一个即可,如果两个都写self.host会采用ip的值。

port是对应连接协议的端口号,netmiko会根据device_type来判断,如果device_type中没有“telnet”,则端口号默认值为22,如果用户赋值了端口号,则采用用户传入的端口号。

用户名密码不用赘述,关于密码的保护,建议代码和密码配置文件分离,或者是在Python代码中使用getpass模块的getpass函数传入密码。

>>> import getpass
>>> x=getpass.getpass()
Password:

secret是类似enable密码的字段,用于给当前用户升权。默认空字符串,只有调用连接对象的enable方法时才会去读取这个字段。

创建连接的时候conn_timeout和timeout一定要根据实际情况进行适当延长:比如show running-config或者display current,当配置量比较大,设备回显会比较慢,有时候需要讲timeout设置为120(两分钟);当设备因为各种原因(比如在两个相隔很远的物理位置)导致登录时间比较长的时候,需要适当调整conn_timeout。

在后续执行信息收集和配置推送的时候,我们可以通过session_log进行完整的操作记录,以便debug,比如排查脚本为什么执行失败,我们推送的配置是否如预期下达等等,默认的记录模式是覆盖,是由session_log_file_mode控制的,它是一个字符串。

fast_cli与global_delay_factor在创建连接类的时候,很多场景下无需修改默认值,结合后续的讲解,大家能了解最好。对于这两个参数的调整可能会加快登录设备以及执行脚本的速度。

3.4 信息收集

进行信息收集在笔者看来是一件性价比非常高的事情,风险小,收益多,任何时刻都可以进行。

netmiko的很多入门也以show命令演示为主,主要用到了netmiko的send_command方法与send_commnad_timing方法。

笔者结合自己的使用以及对源代码的研读,将为大家认真讲解一下,这两种方法主的基本使用、二则差别以及在特定场景下二者使用场景的拓展。

首先二者的共同点要先说明一下,二者都是用于发送单条命令的方法,一般用于show命令,其实二者都可以完成配置推送,但笔者认为需要深刻了解二者的一些细节,才能比较好的实现这个配置推送的效果。

3.4.1 send_command基于pattern的命令执行

"""Execute command_string on the SSH channel using a pattern-based mechanism. Generally
used for show commands. By default this method will keep waiting to receive data until the
network device prompt is detected. The current network device prompt will be determined
automatically.

以上是send_command是代码doc_string中的描述,简单翻译下是:send_command在ssh隧道中执行命令,主要用于show命令。它是基于正则机制的,它执行命令后会持续收集回显,直至收到网络设备提示符,当前网络设备的提示符会由netmiko自动判定。

所谓提示符是指每次我执行命令结束后设备停住的那行,等待我们下一条命令而显示的文本,一般是设备名与#]>等的组合,有时候是提示我们输入密码等等。

netmiko会自动决定提示符,它会在登录结束后输入一个回车,回显是什么提示符就是什么,netmiko会整理成一个正则。这样我们给netmiko一个cmd指令,netmiko执行后不断在ssh隧道中读取回显,直到通过正则断定指定的提示符出现,它认为回显已经结束,将所有回显组装后返回给我们。

以上是send_command方法的基本逻辑,也就是所谓的基于pattern的命令执行,让我们来执行一段代码感受一下:

from netmiko import ConnectHandlerdev = {'device_type': 'cisco_ios','host': '192.168.137.201','username': 'netdevops','password': 'admin123!','port': 22}with ConnectHandler(**dev) as conn:output = conn.send_command(command_string='show running-configuration')print(output)

代码执行结束后会打印设备的show running-configuration回显 。

3.4.2 send_command_timing基于时间延迟的命令执行

"""Execute command_string on the SSH channel using a delay-based mechanism. Generally
used for show commands."""

netmiko中对于发送命令有两个非常重要的方法,一个是基于pattern的send_command,另一个则是基于时间策略的send_command_timing。从send_command_timing的描述中我们了解到两个信息:

  1. 它是一种基于延迟机制的方法。(一会我们会详解这种延迟机制)
  2. 它主要用于show命令。(实际在一些特殊的配置推送场景,它也会有奇效,前提是我们需要吃透这个方法和复杂场景的交互)

netmiko的send_command_timing是基于时间延迟的一个执行命令的方法。从它的名字我们就可以知道它与send_command最大的区别是“timing”,它是基于时间去自动判断执行命令是否结束。发送命令到ssh隧道后,它会进入一个循环,读取ssh隧道中的回显,如果发现回显则证明ssh隧道中还可能有回显在传输过程中,它会让程序暂定一定时间间隔,它调用了time模块的sleep函数来暂停程序,时间间隔比较短暂,是loop_delay乘以延迟因子,loop_delay是0.1秒,延迟因子默认是1,延迟之后进入下一次循环。结束循环的条件有两种:

  1. 超时,在创建netmiko连接的时候我们会传入一个timeout的参数,这个值除以每次延迟的实际时间,就是总循环的次数max_loops,当循环次数超出max_loops的时候,方法结束,将所有的回显返回给调用方。这种情况其实是一种非正常结束的情况,回显基本是不完整的,实际还可以再在ssh隧道里获取数据,只不过在send_command_timing判断已经超时了,就整理一下回显返回给用户,同时这个过程不会抛出错误。

  2. send_command_timing认为回显已经收集结束,它判断的依据是,如果发现没有回显,证明可能隧道中已经无回显或者是设备卡顿了一下,所以它选择停止延迟因子乘以2秒,延迟因子默认值为1 ,也就是说延迟2秒,2秒内还没有任何消息,那就认为回显结束了,退出把回显全部返回;如果还有回显则认为可能是设备卡顿导致刚才的回显是暂无的,所以它进入方法最初的大循环中,继续以小间隔尝试读取回显。

这就是send_command_timing的大体逻辑。它自动判断结束的机制是基于时间延迟的,当回显为空,延迟一个固定时间(默认两秒)仍然为空则认为回显结束。对于回显比较“干净利索”的网络设备而言,这个逻辑没问题,但在实际生产中我们发现用send_command_timing执行一些配置备份时遇到了配置备份不完整的现象,主要是生产环境中部分设备配置量偏大,比如执行show run之后,会在“building configuration”阶段卡了2秒以上导致的。现网中这种情况,笔者觉得还是有一定概率发生的,这个时候建议使用send_command,或者适当调大延迟因子delay_factor。

下面我们来看一下send_command_timing的代码

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22}with ConnectHandler(**dev) as conn:output = conn.send_command_timing(command_string='display current-configuration')print(output)

原本send_command方法,替换成了send_command_timing,其他未做任何变化。send_command_timing方法是基于时间延迟机制的,如果我们想适当扩大判断回显结束的停顿时间,我们该修改哪个参数呢?

在初始化连接不做任何变动的基础之上,我们只需在方法中给delay_factor(默认值为1)赋值即可,适当调整大一些,则此方法最后一次判断回显结束的时间会以延迟因子倍数级增长,从而避免回显不完整的情况发生。

with ConnectHandler(**dev) as conn:#判断回显为空停顿所需的时间是2秒*delay_factor,此处会是延迟6秒来判断回显是否完整output = conn.send_command_timing(command_string='display current-configuration',delay_factor=3)print(output)

这时我们将上述代码的send_command_timing重添加一个delay_factor参数,赋值为2,这个时候判断回显是否完成的时间就变为了4秒。我们可以通过代码打印时间,或者自行计时,两次代码时间相差4秒,正好是延迟因子的差值乘以2秒。

3.4.3 send_command与send_command_timing高级参数详解

以上我们讲解了发送命令(主要执行show命令)的两种方法:基于pattern正则机制的send_command与基于时间延迟机制的send_command_timing。这两个方法的参数极度相似,唯一的区别是send_command支持通过expect_string传入正则(不传入的时候以自动检测的为准,在show命令中此参数意义不大,我们后续会详解解读),send_command_timing支持通过delay_factor来控制判断回显是否结束的延迟时间(这个参数已经讲解过,但是它仍有很多变数在其中,后续我们也会详细解读)。除了这两个参数的区别以及围绕着这两个参数的一些其他参数,这两个方法的剩余参数基本一致,接下来我们会大家详细讲解这些基本的参数,这些平时用不到的参数,在某些特定场景下都会影响我们的程序是否能够如预想的执行。

3.4.3.1 send_command参数详解

expect_string

用于判断回显是否结束的正则表达式,如果不赋值或者传入为空,则由netmiko自动判断结束的提示符(含有设备名称的提示符)。

对于show命令而言,这个参数一般无需修改,官方也说过send_command主要用于show命令,但是基于此参数,send_command方法是可以实现配置推送的。

比如我们要进入设备的配置模式,华为设备需要输入system-view这个时候提示符会有普通的]结束改为#结束,如果直接调用send_command方法执行此命令,而不修改expect_string会让netmik继续在回显中查找原先的提示符的正则(以]结束的设备名称),显然是会失败的,会抛出错误,无法获取到预期的正则,超时报错。这个时候我们需要修改此参数,根据当前输入命令实际的提示符的特征来赋值,比如输入system-view的特征是还有`]

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','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='system-view')print(output)

我们这段代码在创建连接的时候使用了session_log,这样可以保留我们和设备所有交互的细节(无论是netmiko自动完成,还是我们人工指定的命令)。这段代码发送了system-view,进入了配置模式,提示符会变为],但是netmiko自动发现的还是之前认知的包含>,它会一直等>出现直到超时报错,提示我们“OSError: Search pattern never detected in send_command: ”。

我们看一下日志:


Warning: The initial password poses security risks.
The password needs to be changed. Change now? [Y/N]:NInfo: The max number of VTY users is 5, the number of current VTY users online is 2, and total number of terminal users online is 3.The current login time is 2022-07-04 01:38:45.The last login time is 2022-07-04 01:33:12 from 192.168.137.1 through SSH.
<netdevops>
<netdevops>
<netdevops>screen-length 0 temporary
Info: The configuration takes effect on the current user terminal interface only.
<netdevops>
<netdevops>system-view
Enter system view, return user view with return command.
[~netdevops]exit

当我们输入system-view时,新出的提示符是[~netdevops],这个时候netmiko一直在找之前提示符<netdevops>(实际是一个正则表达式),自然就会报错。

这个时候我们可以通过expect_string来写一个正则表达式,写的时候我们可以写的简单一些,只写最后的提示符(正则表达式在各种场景下适合即可,有时候不必过分细化)。

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','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='system-view',expect_string=r']')print(output)

通过在send_command方法中赋值expect_string,告诉netmiko我们希望用]来判定回显结束,这样这段代码就可以顺利执行。

这段代码实际是实现了类似连接类当中的config_mode方法,帮助我们进入配置模式,我们仅是用于演示expect_string的功能。

通过这个参数,我们实际可以将send_command用于特定场景的配置下发,比如软件版本升级时用来传输介质,甚至是整个升级过程,我们可以通过send_command方法完成,只要调整好expect_string即可。如果是一些无交互的场景,我们建议大家使用下一章节的配置推送推荐的方法。

delay_factor

延迟因子,默认值为1。这个参数在netmiko内部很重要,对于普通用户无需特别关注,但是在一些特殊场景之下,这个参数会显得尤为重要。netmiko内部充斥着while循环,很多环节都需要发送命令,并开启一个while循环,以某个时间值进行停顿,去ssh隧道读取数据。这个停顿的时间值是由基数乘以延迟因子决定的,在一些特殊情况下,延迟调小会提高效率,因为这意味着读取ssh隧道数据中的频率会提高,进而节省一些时间。在一些情况下,延迟调大,可以保证数据回显的完整,或者确保设备执行完了我们发送的命令。调节这个,一些方法内部的时间基数我们调整不了,但是我们可以调整延迟因子,进而干预延迟的时间。在不同情况下优化性能或者保证质量。

但是这个延迟因子不是单独作用的,netmiko的连接类在初始化的时候会有global_delay_factor和fast_cli两个参数。global_delay_factor是初始化时的全局延迟因子,fast_cli是在某些情况下进行回显优化的一个参数,大多数设备连接类默认fast_cli为False。当fast_cli模式关闭(即其值为False)的时候,在具体执行时的延迟因子取global_delay_factor和delay_factor中的较大者。反之则取较小者。

在send_command方法中,我们如果希望程序速度提升,可以设置fast_cli为True,并在发送命令的方法中将此参数调整小一些。

max_loops

最大循环次数,默认值为500。它与delay_factor共同控制超时时间。send_command方法发送命令后,会进入一个while循环中以某种时间间隔读取ssh隧道中的数据,延迟间隔的时间是延迟间隔时间基数(默认0.2秒)乘以延迟因子决定的。创建netmiko连接类时的timeout时间除以实际的延迟间隔时间即为这次while循环的最大次数。

当方法中发现max_loops和delay_factor是默认值的时候,会根据刚才的逻辑计算出实际的max_loops。否则则使用用户传入的max_loops。这个值不建议大家修改。我们通过某种方式自己计算max_loops值,可能会有遗漏的地方,导致计算出的数值出现差错;如果是随意传入一个我们认为的max_loops,会导致超时时间与我们预期的不符。无论哪种方式,都是对程序不利的。

auto_find_prompt

自动发现提示符,类型布尔值,默认值为True。当用户调用send_command方法时,未传入expect_string的时候,netmiko会根据此参数决定是否自动发现提示符,其逻辑是发送一个回车到ssh隧道中,然后等待一段时间后从ssh隧道中读取设备发送的回显,普通情况下多为设备名称加一些特殊符号。如果用户传入了expect_string的时候,则此功能会自动取消。

所以当我们发送普通的命令时,如果开启了session_log,会发现每次执行命令前都会有一次netmiko自动发送的换行,这就是此参数请作用的结果。

strip_prompt

去除回显的设备提示符左右空白符,默认值为True。send_command方法中没有修改的意义,建议不修改此默认值。

strip_command

去除回显中发送命令行的左右空白符,默认值为True。send_command方法中没有修改的意义,建议不修改此默认值。

normalize

发送命令行规范标准化,默认值True。它用于确保每个命令行最右侧有一个换行符(简单理解为回车)。通过之前的代码我们知道,send_command(包括send_command_timing)发送命令的时候无需添加回车,就是因为这个参数设置为True,netmiko帮我们自动添加的。其基本逻辑是去除命令行最右侧的空白符,添加一个换行符后发送给设备。

cmd_verify

确认命令行回显,默认值为True。我们发送的命令,在交互中设备也会回显出来,它会出现在ssh隧道中,send_command方法的逻辑是先读取设备返回的我们输入命令,然后再读取到设备配置提示符的正则。这个参数在show命令场景下不是很常用,使用默认值即可,因为绝大多数回显都会显示我们输入的命令。在一些特殊情况下,比如修改密码,我们敲入的字符设备是不会回显的。这个时候我们需要将这个参数设置为False,否则send_command方法会卡在等待我们输入命令的回显的阶段,进而超时报错。

我们以一台cisco的ios交换机演示。

整个交互如果是人工使用软件登录过程如下:

netdevops01>enable
Password:
netdevops01#

我们在使用代码的时候会发现,输入的password后是没有回显的,我们这个时候的新的提示符也是#

如果我们不了解cmd_verify的参数意义,可能会这么写:

from netmiko import ConnectHandlerdev = {'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)

这段代码最后会报错

netmiko.ssh_exception.NetmikoTimeoutException: Timed-out reading channel, data not available.

我们查看log也会发现


netdevops01>terminal width 511
netdevops01>terminal length 0
netdevops01>
netdevops01>
netdevops01>enable
Password:
netdevops01#
netdevops01#exit

实际已经完成了交互,但是netmiko底层机制一直在等待我们输入的密码的回显,最终导致超时。我们只需要在输入密码的那行代码,把cmd_verify

赋值为False即可,这样send_command则不会等待我们输入出现。

from netmiko import ConnectHandlerdev = {'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='#', cmd_verify=False)print(output)

结果代码顺利完成没有报错。

这段代码我们演示了cmd_verify的作用,有些设备的驱动连接类编写的并不是很完善,比如老旧的华为华三设备,提权的时候输入super会提示我们输入密码,就可以将上述代码修改一下,完成当前用户的提权。

我们再扩展一下,这种命令不回显的多是涉及到密码的交互,比如修改密码等也可以使用类似的代码。

textfsm、ttp、genie数据化输出

netmiko自身与一些优秀的第三方网络配置解析包相结合,比如textfsm、ttp与genie。它们可以从我们的文本中提取出格式化的数据,这个功能在我们后期的NetDevOps之旅当中非常有用。我们有时候执行命令可能是想从配置当中获取数据,比如软件版本、端口配置、路由配置等等。

本书会在后续章节着重讲解基于textfsm的配置解析。netmiko安装时会自动安装textfsm与ntc-templates(一个内置了400个左右textfsm解析模板的库),如果内置有对指定命令回显的解析模板,则netmiko会自动将配置解析出结构化的数据(字典的列表),如果无解析模板则返回原始的回显文本(字符串)。

3.4.3.2 send_command_timing参数详解

delay_factor

延迟因子,默认值为1。send_command_timing是基于时间延迟机制的,这个延迟因子主要控制最后一次确认无回显的间隔时间,最后一次的间隔时间默认为2秒(基数2秒乘以延迟因子1),在一些网络环境不佳(跨地域)或者设备输出回显时会在某个环节卡顿的,要适当增加此延迟因子,防止回显不全。

max_loops

同send_command的max_loops,其默认值是150。这个值是多少在很多情况下不重要,因为在程序内判断当前给的值如果和netmiko中此方法默认值相同,netmiko会根据初始化时的超时时间计算出循环次数。

strip_prompt

同send_command的strip_prompt。

strip_command

同send_command的strip_command。

normalize

同send_command的normalize。

cmd_verify

同send_command的cmd_verify。

cmd_echo

同cmd_verify,但是4.0版本后弃用,为保证代码的兼容性,不建议使用。

3.4.4 配置备份实战

当有了netmiko的send_command的时候,我们的NetDevOps之旅会有一种豁然开朗的感觉,我们可以用它来做什么呢?

这个时候我称之为开脑洞的时间,大家一定要结合自己的日常运维充分发挥自己的想象力,一场自己和自己的头脑风暴之后,你会有一些可以落地的脚本方案,在经历过一番折腾之后,完成了你和网络设备打交道的第一个有用的脚本,你会有一种非常愉悦的感受,这也是NetDevOps的乐趣所在。

言归正传,笔者总结在这个阶段,我们可以做的事情主要是以下两个方面:

  1. 配置备份
  2. 巡检数据收集

3.4.4.1 基于netmiko的配置备份

网络的配置备份是大多数网络工程师NetDevOps之旅的第一站。作为网络工程师我们需要定期登录设备,执行show running-configuration等命令获取网络设备的配置,同时将其保存到文本中,后续进行一些配置的比对或者数据的提取。然后一台又一台的设备重复类似的过程,直至几十上百甚至上千台设备全部完成以上内容。这种重复、机械、明确的工作非常适合通过自动化来完成。接下来我们就带领大家一步步来实现这个配置备份的场景。

基于刚才send_command的示例,我们添加保存文本的代码。

from netmiko import ConnectHandlerdev = {'device_type': 'cisco_ios','host': '192.168.137.201','username': 'netdevops','password': 'admin123!','secret': 'admin1234!','port': 22}with ConnectHandler(**dev) as conn:conn.enable()output = conn.send_command(command_string='show running-configuration')with open('192.168.137.201.txt', mode='w', encoding='utf8') as f:f.write(output)

这段代码的改变在于加了secret参数,因为有些命令的执行(尤其是查看配置或者进入配置模式)需要进入类似思科的enable模式,进行账户的升权。我们需要先给用于升权的secret赋值,然后在连接到设备后,执行升权的方法enable,netmiko会自动帮我们完成升权。有些设备没有相关提权的操作(这种无提权可能是设备真没有提权操作,也可能是netmiko不支持此设备的提权),我们可以不用赋值secret,也无需调用enable方法。比如华为设备的netmiko驱动中是无提权模式的,它重写的enable是一个空方法,无任何执行动作,即使我们调用也不会报错。后续的演示我们将以华为设备为示例。

我们在创建了到设备的连接对象conn后,调用连接对象的enable方法,netmiko会帮助我们自动进入设备的enable模式。

进入enable模式后,使用send_command方法,传入要执行的查看配置的命令即可,其中command_string是第一个参数,我们实际可以不写参数名,方法后直接跟要执行的命令字符串即可(回想我们针对初学者函数赋值的最佳实践)。我们创建了一个output变量,用于接收执行命令后的回显。

最后通过之前章节所学的文本文件的写入即可,在这里我们再次强调几个点:

  1. 使用with上下文管理创建文件
  2. 显式的赋值mode和encoding
  3. 字符集一定要使用utf8

执行完代码,我们就会在代码的同级目录看到一个192.168.137.201.txt的配置文件。

至此我们完成了一台设备的配置备份,但是这段代码还是差一些意思,我们只执行了一台设备的配置备份,我们需要执行多台设备的配置备份,这个时候我们考虑到这个过程是“重复”的,我们可以把一台设备的配置备份封装成一个函数,这样多台设备只需要循环调用即可。所以,我们上面的代码封装成一个函数。

此后的代码我们将以华为CE6800交换机作为演示对象。

from netmiko import ConnectHandlerdef network_device_backup(dev, cmd='display current-configuration'):"""登录设备执行配置备份的命令,并将设备回显的配置写入一个<设备IP或者host>.txt的文件中,编码格式utf8:param dev: 设备的基础信息,类型字典,key与创建netmiko所需的参数对应:param cmd: 要执行的配置备份的命令,默认是华为的“display current-configuration”:return: None 不返回,只打印"""with ConnectHandler(**dev) as conn:conn.enable()output = conn.send_command(command_string=cmd)file_name = '{}.txt'.format(dev['host'])with open(file_name, mode='w', encoding='utf8') as f:f.write(output)print('{}执行备份成功'.format(dev['host']))if __name__ == '__main__':dev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22}network_device_backup(dev)

代码执行成功后会打印“192.168.137.201执行备份成功”。

在这段代码里,我们封装了一个名为network_device_backup的函数,它用于实现配置备份的功能,执行逻辑是使用netmiko连接设备,执行配置备份的命令并写入一定格式的文本文件中。所以它有两个参数:

  1. dev:连接设备所需的基础信息,类型字典,key与创建netmiko连接所需的参数对应
  2. cmd: 要执行的配置备份的命令,假如我们的网络环境是以华为设备为主,我们可以给这个参数一个默认值“display current-configuration”,这样调用函数的时候,我们就无需传入cmd参数了。

函数的代码逻辑中我们执行了enable方法,在华为的驱动中这个方法是空的,所以不影响整体使用。在一些驱动中比如华三的驱动中是进入了system-view模式,但是执行查看配置的命令也是没问题的。我们要根据设备的实际情况判断是否调用enable方法。整体而言,在账号权限足够的情况下,调用此方法无报错的风险。

定义好了函数之后,我们在调用的时候用了if __name__ == '__main__的形式,这个有点类似于其他语言中的main函数。我们写的Python代码,有两种执行情况:

  1. 作为脚本被Python解析器直接调用执行
  2. 被其他脚本导入(import)作为模块被按需调用执行

if __name__ == '__main__'代表这个脚本是被直接调用执行,而非被其他脚本引用。Python中万物皆对象,包括Python脚本也可以视为对象,这个对象中有一个属性是__name__,如果作为脚本被Python解析器直接调用执行,则其值为__main__,所以这个判断的作用是,当脚本被独立调用执行的时候,会执行这段业务逻辑。如果被其他脚本导入这个脚本的network_device_backup函数,则不会执行。

当然我们这段代码逻辑对应的是第1种场景,直接作为脚本执行,同时也为这个脚本中函数复用预留了空间。

3.4.4.2 设备批量配置备份

以上是单台设备的一个配置备份脚本,网络运维有一大特点是重复与机械,原因是我们往往面对的是很多网络设备。所以上述脚本仍无法用于实战,因为缺少对批量操作。接下来我们结合之前所学,进一步完成网络设备批量配置备份的功能。

首先,我们要面临的问题是,用什么形式承载众多网络设备?用什么方法实现批量?

首先看第一个问题,用什么形式承载众多网络设备。个人建议使用表格,集合我们之前所讲解的表格处理,我们可以通过Excel或者csv等表格文件承载网络设备的信息,通过pandas将表格加载成字典的列表,表格管理方便、代码量少、可读性强、使用方便。当然也有一些分享是基于文本文件的,需要用很多代码去提取相关信息,且文本文件添加一台设备也可能出现各种问题,比如间隔符号中英文的问题,漏写字段等等。

我们先来定义一个表格,用于承载网络设备和执行的备份命令。

host device_type username password secret timeout conn_timeout backup_cmd
192.168.137.201 huawei netdevops Admin123~ 180 20 display current-configuration
192.168.137.202 huawei netdevops Admin123~ 180 20 display current-configuration
192.168.137.203 cisco_ios netdevops admin123! admin1234! 180 20 show current-configuration

其中前面几个字段与netmiko创建连接所需的参数完全一致,最后一个用于记录需要执行的配置备份的命令。

我们写一个函数来读取这个网络设备清单,分别返回配置备份函数network_device_backup所需的两个参数。

import pandas as pddef get_batch_backup_dev_infos(filename='inventory.xlsx'):'''读取Excel表格加载网络设备基本信息和其配置备份的命令,结果返回一个元组的列表:param filename: 表格名称,默认值是inventory.xlsx:return: [(<netmiko连接设备所需基本信息的字典>,<配置备份的命令>)]示例:返回的结果列表,成员是以表头为key的字典[({'host': '192.168.137.201','device_type': 'huawei','username': 'netdevops','password': 'Admin123~','secret': nan,'timeout': 180,'conn_timeout': 20},'display current-configuration'),]'''# 读取并将表格加载成字典的列表df = pd.read_excel(filename)items = df.to_dict(orient='records')# 构建返回的结果,dev_infos是一个元组的列表。dev_infos = []for i in items:# 取出配置备份的命令,并用del 将其从字典中删除backup_cmd = i['backup_cmd']del i['backup_cmd']# 删除配置备份命令后的字典就是netmiko登录设备所需的信息dev = idev_infos.append((dev, backup_cmd))return dev_infos

这段代码主要包含了两大块逻辑,第一块逻辑是读取表格,将其转换为字典的列表,短短两行代码,非常的方便且易读。比用文本文件承载网络设备清单实在是方便太多。第二块逻辑是构建新的数据,用于与配置备份函数network_device_backup对接。

当这块代码写好后,我们可以和之前单台配置备份设备的代码结合起来,用for循环实现多台设备的批量配置备份。

import pandas as pd
from netmiko import ConnectHandlerdef get_batch_backup_dev_infos(filename='inventory.xlsx'):'''读取Excel表格加载网络设备基本信息和其配置备份的命令,结果返回一个元组的列表:param filename: 表格名称,默认值是inventory.xlsx:return: [(<netmiko连接设备所需基本信息的字典>,<配置备份的命令>)]示例:[({'host': '192.168.137.201','device_type': 'huawei','username': 'netdevops','password': 'Admin123~','secret': nan,'timeout': 180,'conn_timeout': 20},'display current-configuration'),]'''# 读取并将表格加载成字典的列表df = pd.read_excel(filename)items = df.to_dict(orient='records')# 构建返回的结果,dev_infos是一个元组的列表。dev_infos = []for i in items:# 取出配置备份的命令,并用del 将其从字典中删除backup_cmd = i['backup_cmd']del i['backup_cmd']# 删除配置备份命令后的字典就是netmiko登录设备所需的信息dev = idev_infos.append((dev, backup_cmd))return dev_infosdef network_device_backup(dev, cmd='display current-configuration'):"""登录设备执行配置备份的命令,并将设备回显的配置写入一个<设备IP或者host>.txt的文件中,编码格式utf8:param dev: 设备的基础信息,类型字典,key与创建netmiko所需的参数对应:param cmd: 要执行的配置备份的命令,默认是华为的“display current-configuration”:return: None 不返回,只打印"""with ConnectHandler(**dev) as conn:conn.enable()output = conn.send_command(command_string=cmd)file_name = '{}.txt'.format(dev['host'])with open(file_name, mode='w', encoding='utf8') as f:f.write(output)print('{}执行备份成功'.format(dev['host']))def batch_backup(inventory_file='inventory.xlsx'):dev_infos = get_batch_backup_dev_infos(inventory_file)for dev_info in dev_infos:dev = dev_info[0]cmd = dev_info[1]network_device_backup(dev, cmd)if __name__ == '__main__':batch_backup()

这段代码结合了两个函数,然后我们又写了一个函数batch_backup来组织批量备份的业务逻辑,这段代码逻辑异常简单,首先是调用了

get_batch_backup_dev_infos函数获取用于批量备份的网络设备的netmiko参数及命令,然后使用for循环,读取每个设备及其命令,调用单台设备的配置备份。整个函数只有5行代码,逻辑性和可读性都非常好,这就是我们把业务逻辑按照一定的功能拆分成函数后的结果,可以让业务逻辑更加清楚,且测试的时候也更加方便。

当然这段代码还有优化的空间,比如配置备份可以根据device_type去判断映射,执行效率层面也可以继续提供,通过多进程或者多线程实现并发。关于前者大家可以根据自己的使用习惯和理解去修正迭代,关于后者通过并发提高执行效率,对于初学者笔者并不建议大家牵扯过多精力去做多线程多进程这种比较底层的Python知识中,这种批量备份配置的脚本,已经可以有效地解放我们的劳动力,在脚本执行期间,我们可以做一些其他工作,等过段时间确认备份完成即可。等大家对Python的理解逐渐深入,我们可以尝试使用多线程多进程,但是笔者更建议大家关注nornir这个自动化框架,基于Python代码可以非常方便地实现批量自动化功能,可以让我们更加专注于网络自动化的业务本身,而不是一些控制进程和线程的代码身上,同时它对于设备的管理也更加便利和多样化。

3.5 配置推送

在网络自动化的场景当中,配置推送是一个刚需且高频的场景。netmiko也有非常好的封装,保住我们非常便捷地实现配置的推送以及配置的保存。

3.5.1 直接发送配置send_config_set

send_config_set支持将配置通过ssh隧道按序发送到设备的功能,并且它会自动进入设备的config模式,配置发送完成,设备接收到之后,它还会自动退出config模式。但是这个方法聚焦去配置的发送,如果涉及到配置的保存,需要调用另外一个方法save_config来进行,使用save_config无需关注当前驱动的配置保存命令是什么,直接调用,netmiko会帮助我们自动输入对应平台的保存命令,并自动完成一部分交互。

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,'session_log': 'netdevops.log'}with ConnectHandler(**dev) as conn:# 如需提权 执行enable# conn.enable()# 将配置放到列表中,实际使用tuple等可以迭代的对象也可以config_cmds = ['interface GE1/0/0','description cofiged by netmiko','commit']# 调用send_config_set,赋值config_commands,返回整个交互过程的回显config_output = conn.send_config_set(config_commands=config_cmds)print('config_output:')print(config_output)# save_config,无需赋值,netmiko已经处理好它所支持的厂商型号的保存过程,返回整个交互过程的回显save_output = conn.save_config()print('save_output:')print(save_output)

其输出结果为

config_output:
system-view
Enter system view, return user view with return command.
[~netdevops]interface GE1/0/0
[~netdevops-GE1/0/0]description cofiged by netmiko
[*netdevops-GE1/0/0]commit
[~netdevops-GE1/0/0]return
<netdevops>
save_output:
save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 1
Info: Save the configuration successfully.
<netdevops>

调用send_config_set方法,最核心的参数是config_commands,也就是我们要发送的配置命令,我们在现阶段简单理解它可以是列表或者元组即可,没个成员是配置中的一行。

从输出是结果我们发现,这个方法帮我们自动执行了system-view和return,即自动进入和退出了配置模式。对于华为的某些版本的交换机,如果涉及到commit提交配置才生效,我们一定要在命令最后追加这一条,某则可能会有一个我们有未提交配置的提示,会让netmiko“卡住”。

PS:实际netmiko对某些型号连接类驱动是支持commit方法的,但是对华为这种国产的设备支持并不是很好,建议初学者自己在使用send_config_set方法中的命令最后追加commit命令。

在将配置推送到设备并生效配置之后,我们希望保存配置,这个时候我们无需输入save命令,我们只需要调用save_config方法,netmiko对很多型号设备进行了适配,会帮助我们自动输入对应平台的保存配置的命令,比如上述示例中的华为CE6800交换机,在我们调用save_config方法后,自动发送了save命令与确认操作,极大简化了我们的开发量,使我们的开发更加聚焦。

部分型号的设备,比如思科很多系列的设备,需要对当前账户进行提权才能进入config模式,我们也可以直接调用netmiko的enable方法进行提权。

from netmiko import ConnectHandlerdev = {'device_type': 'cisco_ios','host': '192.168.137.201','username': 'netdevops','password': 'admin123!','secret': 'admin1234!','port': 22}with ConnectHandler(**dev) as conn:# 如需提权 执行enableconn.enable()# 将配置放到列表中,实际使用tuple等可以迭代的对象也可以config_cmds = ['interface Eth0/1','description cofiged by netmiko']# 调用send_config_set,赋值config_commands,返回整个交互过程的回显config_output = conn.send_config_set(config_commands=config_cmds)print('config_output:')print(config_output)# save_config,无需赋值,netmiko已经处理好它所支持的厂商型号的保存过程,返回整个交互过程的回显save_output = conn.save_config()print('save_output:')print(save_output)

其输出结果为:

config_output:
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
netdevops01(config)#interface Eth0/1
netdevops01(config-if)#description cofiged by netmiko
netdevops01(config-if)#end
netdevops01#
save_output:
write memBuilding configuration...
Compressed configuration from 1251 bytes to 843 bytes[OK]
netdevops01#

在代码当中,由于是思科的ios设备,需要enable对当前账户提权,这个时候我们必须在初始化连接的参数中赋值secret参数,其对应的是enable的密码。然后在使用send_config_set方法给设备下发配置前,调用了netmiko自带的enable方法,netmiko根据当前设备的驱动连接类,自动帮我们输入了enable命令和后面的enable密码(即secret参数中的密码)。

如果这个enable方法实现的不太好,比如华三的一些特殊型号的设备,我们也可以按照之前send_command的方法进行提权,注意调配好参数即可,之后再调用send_config_set发送命令即可。此处不再赘述。

参数详解

config_commands

要推送给设备的配置,推荐使用列表或者元组。此参数也接受字符串,但是会被认为是一条命令,我们如果试图在字符串中自己添加换行,由于netmiko对回显的判断机制,这个方法实际是行不通的,综上笔者不推荐使用字符串。

enter_config_mode

是否进入配置模式,默认值为True。如果此参数为True,netmiko会调用config_mode方法,进入配置模式。这个config_mode方法中会有一个config_command的参数,各厂商的驱动都会重写此方法,赋值为其实际的进入配置模式的命令,如下示例中,这个是华为netmiko连接类中的config_mode方法的重写,它将config_command赋值为华为的"system-view".

def config_mode(self, config_command="system-view"):"""Enter configuration mode."""return super().config_mode(config_command=config_command)
config_mode_command

进入配置模式的命令,默认值为None。如果用户选择进入配置模式(参考enter_config_mode),一般各家驱动都会重写config_mode,内置了对应的进入配置模式的命令,所以在send_config_set中会有机制进行判断,如果用户传入的config_mode_command为None,则不会给config_mode的config_command赋值,从而使用驱动内置的config_command的默认值。此参数大家了解即可,一般已经适配的平台基本无需调整此参数的值。

exit_config_mode

是否退出配置模式,默认值是True。如果此参数为True,netmiko会调用exit_config_mode方法,退出配置模式。exit_config_mode方法中会有一个exit_command的参数,同样各厂商的驱动也会重写此方法,赋值为对应平台退出配置模式的命令,如下示例中,是华为驱动的重写。

def exit_config_mode(self, exit_config="return", pattern=r">"):"""Exit configuration mode."""return super().exit_config_mode(exit_config=exit_config, pattern=pattern)

不过send_config_set方法中没有对应离开配置模式的命令的参数。实际在send_config_set方法中进入配置模式的命令的参数意义也不大。毕竟进入与退出配置模式的方法都会重写,根据平台实际情况调用各自的命令。

error_pattern

在回显中判断配置是否有错误的正则表达式。默认为空字符串,即认为错误命令判断机制不会生效,netmiko会逐行推送配置,无论每条命令是否成功。如果根据各平台特性配置错误的正则表达式,则在输入配置命令行,回显中出错时,netmiko会自动停止配置下发,并抛出异常。

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,'session_log': 'netdevops.log'}with ConnectHandler(**dev) as conn:# 故意对一个不存在的端口进行配置config_cmds = ['interface GE2/0/0', 'description cofiged by netmiko', 'commit']# error_pattern进行赋值,如果某行命令回显符合正则则会抛出异常,代码终止,配置不会继续下发config_output = conn.send_config_set(config_commands=config_cmds, error_pattern=r'Error:')print('config_output:')print(config_output)save_output = conn.save_config()print('save_output:')print(save_output)

在上述代码中,我们在调用send_config_set的时候,赋值了error_pattern,因为华为的错误提示有很多种,其共同的特点是显示"Error:"字样,所以此处我们设置为"Error:"作为其正则。

上述代码由于对一个不存在的端口进行了配置,且我们赋值了error_pattern,则当发送第一行配置的时候,netmiko获取到“Error: Wrong parameter found at ‘^’ position.”的回显,符合正则,程序会终止并抛出异常。

    raise ConfigInvalidException(msg)
netmiko.ssh_exception.ConfigInvalidException: Invalid input detected at command: interface GE2/0/0

实际错误回显的正则可以是很多,比如华为的错误提示中多有'^'

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,'session_log': 'netdevops.log'}with ConnectHandler(**dev) as conn:# 故意对一个不存在的端口进行配置config_cmds = ['interface GE2/0/0', 'description cofiged by netmiko', 'commit']# error_pattern进行赋值,如果某行命令回显符合正则则会抛出异常,代码终止,配置不会继续下发config_output = conn.send_config_set(config_commands=config_cmds, error_pattern=r"'^'")print('config_output:')print(config_output)save_output = conn.save_config()print('save_output:')print(save_output)

当我们运行上述代码时会发现没有按预期抛出异常终止配置下发。

config_output:
system-view
Enter system view, return user view with return command.
[~netdevops]interface GE2/0/0^
Error: Wrong parameter found at '^' position.
[~netdevops]description cofiged by netmiko^
Error: Unrecognized command found at '^' position.
[~netdevops]commit
[~netdevops]return
<netdevops>
save_output:
save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 17
Info: Save the configuration successfully.
<netdevops>

我们明明配置了error_pattern,在打印的输出中设备明明回显了“Error: Wrong parameter found at ‘^’ position.”。正则感觉是符合的,为什么不生效呢?

这是因为在书写错误正则的时候,我们一定要注意正则表达式中的转义取消,'^'中的^在正则中代表的是“开始”,是有特殊含义的字符,而不是字符串本身。如果我们想让它表示字符串本身,就需要用反斜杠\取消这个字符串的特殊含义,这就是取消转义,这样\^就可以代表^字符本身了。这一点也使用适用于其他有特殊含义的字符或者字符组合,相关细节,我们会在后续的正则表达式篇章展开讲解。

按照上述思路修改代码:

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,'session_log': 'netdevops.log'}with ConnectHandler(**dev) as conn:# 故意对一个不存在的端口进行配置config_cmds = ['interface GE2/0/0', 'description cofiged by netmiko', 'commit']# error_pattern进行赋值,注意取消转义config_output = conn.send_config_set(config_commands=config_cmds, error_pattern=r"'\^'")print('config_output:')print(config_output)save_output = conn.save_config()print('save_output:')print(save_output)

结果会抛出异常,并终止后续的配置推送及整个程序。

    raise ConfigInvalidException(msg)
netmiko.ssh_exception.ConfigInvalidException: Invalid input detected at command: interface GE2/0/0

在这段代码中,我们会注意,在error_pattern赋值的字符串前面我们加了个’r’,它是干什么用的呢?

这个是取消字符串中的转义,刚才我们讲的是正则表达式中的转义取消,在字符串中也有转义一说,一个反斜杠加上字符就可以代表一个特殊的字符,这个过程就是转义,比如大家最熟知的就是\n代表换行,其中反斜杠是转义符,用于将字符转义(赋予一个新的含义)。那在error_pattern中我们如果想取消正则表达式中的转义会写一个反斜杠,但是这个在字符串中代表的是字符串的转义,为了取消这个字符串中的转义,让它代表反斜杠字符串本身,我们需要对它取消转义。

那 error_pattern的赋值形式是 error_pattern="'\\^'",其中左数第一个反斜杠代表取消转义,而第二个反斜杠是字符串中的转义符,二者相结合就是取消字符串中的转义符功能,结果二者组合才实现了真正的单纯的反斜杠字符本身的原始样貌,后接正则中代表开始的^,取消其正则中的特殊含义,最终代表了我们视觉上'^'的效果。由于正则表达式要涉及到众多的特殊含义的字符,如果我们想取消其正则转义,要如上述示例写大量的双反斜杠,这样显得很赘余,所以我们在字符串外侧最前添加r就可以取消字符串转义,字符串中写的是什么,字符只表示显示上的含义。大家在此阶段理解即可,后面会随着对字符串和正则的深入了解而逐渐清晰明朗。

以上几个参数就是send_config_set的核心参数,剩下的几个参数和send_command中的意义几乎一致,且很少调整,大家了解即可。

delay_factor

延迟因子,默认值为1。其基本原理与send_command一致,不建议调整,因为不同于show命令,配置的命令刷入设备时延小,回显快。此参数调整的意义并不是很大。

max_loops

同send_command最大循环次数,但是其默认值为150。原因同上。

strip_prompt

去除回显的设备提示符左右空白符,默认值为False。可能是出于发送的配置要“原汁原味”,不容篡改的考虑,所以设置为了False。建议不修改此默认值。

strip_command

去除回显中发送命令行的左右空白符,默认值为False。s建议不修改此默认值。

cmd_verify

确认命令行回显,默认值为True。原理同send_command,由于配置几乎都会回显,且确认回显可以保证设备接受到了配置命令,所以不建议修改默认值。

3.5.2 从文本文件中加载并发送配置send_config_from_file

send_config_from_file实际是一个send_config_set的一个“变种”,send_config_set方法需要将配置放在一个列表的变量当中,代码和配置有一定耦合性。send_config_from_file如方法名提到的,通过文件发送配置给设备。这个文件是UTF-8的编码格式的文本文件,配置的命令行如同我们日常使用那般,逐行书写即可,此方法也会逐行推送到设备。send_config_from_file和send_config_set的方法差异就是存放配置的参数,send_config_from_file方法的参数是config_file,它是存放配置的文本文件路径。

示例如下:

from netmiko import ConnectHandlerdev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,'session_log': 'netdevops.log'}with ConnectHandler(**dev) as conn:# 调用send_config_from_file,赋值config_file为写有我们预下发配置的文本文件即可。# 文本文件一定要用utf8的字符集。config_output = conn.send_config_from_file(config_file='config.txt')print('config_output:')print(config_output)# save_config,无需赋值,netmiko已经处理好它所支持的厂商型号的保存过程,返回整个交互过程的回显save_output = conn.save_config()print('save_output:')print(save_output)

结果

config_output:
system-view
Enter system view, return user view with return command.
[~netdevops]interface GE1/0/0
[~netdevops-GE1/0/0]  description cofiged by netmiko
[*netdevops-GE1/0/0]interface GE1/0/1
[*netdevops-GE1/0/1]  description cofiged by netmiko
[*netdevops-GE1/0/1]interface GE1/0/2
[*netdevops-GE1/0/2]  description cofiged by netmiko
[*netdevops-GE1/0/2]commit
[~netdevops-GE1/0/2]return
<netdevops>
save_output:
save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 17
Info: Save the configuration successfully.
<netdevops>

send_config_from_file多了config_file,没有config_commands参数,其他的参数与send_config_set完全一致。实际在内部它读取了文本的内容,整理成命令行的列表,加上其他参数一起透传调用了send_config_set方法。

通过文本文件承载配置内容,在某种程度上可以实现代码和配置的解耦。比如上述代码,我们不仅可以发送端口修改的配置,只要我们修改配置文件,还可以修改访问控制、syslog服务器等众多其他配置,且无需修改代码。

3.5.3 批量配置推送实战

基于netmiko的批量配置推送实际有很多种方式,我们先在此简单介绍一种基于send_config_from_file方法的批量配置推送,在后续的章节中我们将逐渐展开,在此之上结合模板引擎Jinja2和自动化框架nornir。之前我们介绍过send_config_from_file方法可以做到预下发的网络配置与脚本分离的效果,我们每次做配置修改,只需要修改对应的配置文件,无需修改脚本,从而实现脚本的不断复用。

简述一下我们的设计思路:

  1. 首先设计一个函数,实现单设备的配置推送,这个函数接受三个参数:创建netmiko连接的基本信息、配置存放的文件、是否进行提权。
  2. 设计一个表格承载每台网络设备进行批量配置推送所需的表头,上述函数可以对接。
  3. 读取表格信息,循环调用1中所述的函数。

首先我们先编写函数,实现单台网络设备的配置推送。

from netmiko import ConnectHandlerdef network_device_config_push_by_config_file(dev, config_file=None, enable=False):"""使用netmiko登录设备,通过config_file中的调用netmiko连接类中的send_config_from_file方法,将配置推送到设备并保存,整个配置推送的过程保存到日志文件当中。:param dev: 设备的基础信息,类型字典,key与创建netmiko所需的参数对应:param config_file: 存放预下发配置的文本文件,utf8格式。如果为None,则以{dev[host]}.txt作为配置文件:param enable: 是否提权当前用户,默认值为False不进行提权。:return: {'push_sucess':True or Fasle # 配置推送是否成功'session_log':配置推送过程的日志文件 # 配置推送的日志文件}"""# 如果config_file传入的值为空,则根据规则读取默认的配置文件if not config_file:config_file = '{}.txt'.format(dev['host'])# 添加配置推送的日志文件session_log = '{}.log'.format(dev['host'])dev['session_log'] = session_log# 初始化返回结果push_result = {'push_sucess': True,'session_log': session_log}try:with ConnectHandler(**dev) as conn:# 如果用户选择开启enable模式 则调用enable方法if enable:conn.enable()push_output = conn.send_config_from_file(config_file)save_output = conn.save_config()except Exception as e:push_result['push_sucess'] = Falseprint('{}配置推送发生异常,使用的配置文件为:{},异常信息:{}'.format(dev['host'],config_file,e))return push_resultif __name__ == '__main__':dev = {'device_type': 'huawei','host': '192.168.137.201','username': 'netdevops','password': 'Admin123~','port': 22,}result = network_device_config_push_by_config_file(dev=dev, config_file='config.txt', enable=False)print(result)

在编写这个函数的时候我们设计了三个参数创建netmiko连接基本信息的dev、配置存放的文件config_file、是否进行提权enable。并对后两个参数添加了默认值。如果config_file为空,则我们会给它一个默认值(见代码)。在函数的开始部分我们初始化了一个返回结果的字典,包含两个字段,分别记录配置是否推送成功和推送过程的日志文件。之后就是创建到设备的连接,视情况进行提权,没然后通过send_config_set_from_file方法读取配置文件推送给设备配置。这段代码我们用了异常捕获的机制,在代码块外侧用关键字try包裹,通过except关键字捕获异常。try内包裹的代码块有异常则会被except捕获并进入到except的代码块当中,except中负责异常处理的代码块,我们设置推送结果为失败,并打印异常相关信息告知用户。

编写完函数之后,我们通过之前所说的if __name__ == '__main__'来进行函数的调用测试,程序正常退出后,会打印出相关函数的调用结果,同时我们可以看到对应的日志文件。

打印的结果:

{'push_sucess': True, 'session_log': '192.168.137.201.log'}

对应的日志文件192.168.137.201.log:

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-11 01:23:47.The last login time is 2022-07-11 01:06:09 from 192.168.137.1 through SSH.
<netdevops>
<netdevops>
<netdevops>screen-length 0 temporary
Info: The configuration takes effect on the current user terminal interface only.
<netdevops>
<netdevops>system-view
Enter system view, return user view with return command.
[~netdevops]
[~netdevops]interface GE1/0/0
[~netdevops-GE1/0/0]  description cofiged by netmiko
[~netdevops-GE1/0/0]interface GE1/0/1
[~netdevops-GE1/0/1]  description cofiged by netmiko
[~netdevops-GE1/0/1]interface GE1/0/2
[~netdevops-GE1/0/2]  description cofiged by netmiko
[~netdevops-GE1/0/2]commit
[~netdevops-GE1/0/2]
[~netdevops-GE1/0/2]return
<netdevops>
<netdevops>save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 1
Info: Save the configuration successfully.
<netdevops>exit

当我们写完这个单台设备配置推送函数之后,我们紧接着要做的就是设计一张表,记录配置推送所需的信息,然后读取表格循环调用函数即可。

我们的表格设计按照之前批量配置备份的思路设计:

host device_type username password secret timeout conn_timeout config_file enable
192.168.137.201 huawei netdevops Admin123~ 60 20 192.168.137.201.config False
192.168.137.202 huawei netdevops Admin123~ 60 20 192.168.137.202.config False
192.168.137.203 cisco_ios netdevops admin123! admin1234! 60 20 192.168.137.203.config True

除了之前配置推送的基本信息以外,添加了两个字段分别是配置文件的名称和是否进行提权。按照之前批量配置备份的思路,我们写一个函数加载用于配置推送的列表。

import pandas as pddef get_batch_config_push_dev_infos(filename='config_inventory.xlsx'):'''读取Excel表格加载网络设备基本信息和配置文件,结果返回一个用于批量配置推送的元组的列表:param filename: 表格名称,默认值是inventory.xlsx:return: [(<netmiko连接设备所需基本信息的字典>,<配置文件>,<是否提权>)]示例:[({'host': '192.168.137.201','device_type': 'huawei','username': 'netdevops','password': 'Admin123~','secret': nan,'timeout': 180,'conn_timeout': 20},'192.168,137.201.config',False),]'''# 读取并将表格加载成字典的列表df = pd.read_excel(filename)# 表格中的部分未填充值的单元格加载到pandas中会是一种特殊的空值nan,为了方便后续处理我们一律填充为空字符串df = df.fillna('')items = df.to_dict(orient='records')# 构建返回的结果,dev_infos是一个元组的列表。dev_infos = []for i in items:# 取出相关配置推送的参数,并用del将其从字典中删除config_file = i['config_file']del i['config_file']# pandas无法保存布尔值,故简单对其进行一个判断# 对表中的数据转为小写,方便比对判断enable = i['enable']del i['enable']# 删除配置备份命令后的字典就是netmiko登录设备所需的信息dev = idev_infos.append((dev, config_file, enable))return dev_infosif __name__ == '__main__':devs = get_batch_config_push_dev_infos()print(devs)

上述代码需要注意的是表格中的单元格如果未填充值,会被识别成一种pandas内的特殊值nan,它代表缺失值,为了方便处理我们通过fillna的方法将这种缺失值统一处理为空字符串。另外我们在表格中写的True和False也被pandas成功转为布尔值,这种识别机制体验非常好。

其运行结果如下:

[({'host': '192.168.137.201', 'device_type': 'huawei', 'username': 'netdevops', 'password': 'Admin123~', 'secret': '', 'timeout': 60, 'conn_timeout': 20}, '192.168.137.201.config', False), ({'host': '192.168.137.202', 'device_type': 'huawei', 'username': 'netdevops', 'password': 'Admin123~', 'secret': '', 'timeout': 60, 'conn_timeout': 20}, '192.168.137.202.config', False), ({'host': '192.168.137.203', 'device_type': 'cisco_ios', 'username': 'netdevops', 'password': 'admin123!', 'secret': 'admin1234!', 'timeout': 60, 'conn_timeout': 20}, '192.168.137.203.config', True)]

至此两个函数编写完成,我们只需再写一个函数将二者成功结合起来即可。

from netmiko import ConnectHandler
import pandas as pddef network_device_config_push_by_config_file(dev, config_file=None, enable=False):"""使用netmiko登录设备,通过config_file中的调用netmiko连接类中的send_config_from_file方法,将配置推送到设备并保存,整个配置推送的过程保存到日志文件当中。:param dev: 设备的基础信息,类型字典,key与创建netmiko所需的参数对应:param config_file: 存放预下发配置的文本文件,utf8格式。如果为None,则以{dev[host]}.txt作为配置文件:param enable: 是否提权当前用户,默认值为False不进行提权。:return: {'push_sucess':True or Fasle # 配置推送是否成功'session_log':配置推送过程的日志文件 # 配置推送的日志文件}"""# 如果config_file传入的值为空,则根据规则读取默认的配置文件if not config_file:config_file = '{}.txt'.format(dev['host'])# 添加配置推送的日志文件session_log = '{}.log'.format(dev['host'])dev['session_log'] = session_log# 初始化返回结果push_result = {'push_sucess': True,'session_log': session_log}try:with ConnectHandler(**dev) as conn:# 如果用户选择开启enable模式 则调用enable方法if enable:conn.enable()push_output = conn.send_config_from_file(config_file)save_output = conn.save_config()except Exception as e:push_result['push_sucess'] = Falseprint('{}配置推送发生异常,使用的配置文件为:{},异常信息:{}'.format(dev['host'], config_file, e))return push_resultdef get_batch_config_push_dev_infos(filename='config_inventory.xlsx'):'''读取Excel表格加载网络设备基本信息和配置文件,结果返回一个用于批量配置推送的元组的列表:param filename: 表格名称,默认值是inventory.xlsx:return: [(<netmiko连接设备所需基本信息的字典>,<配置文件>,<是否提权>)]示例:[({'host': '192.168.137.201','device_type': 'huawei','username': 'netdevops','password': 'Admin123~','secret': nan,'timeout': 180,'conn_timeout': 20},'192.168,137.201.config',False),]'''# 读取并将表格加载成字典的列表df = pd.read_excel(filename)# 表格中的部分未填充值的单元格加载到pandas中会是一种特殊的空值nan,为了方便后续处理我们一律填充为空字符串df = df.fillna('')items = df.to_dict(orient='records')# 构建返回的结果,dev_infos是一个元组的列表。dev_infos = []for i in items:# 取出相关配置推送的参数,并用del将其从字典中删除config_file = i['config_file']del i['config_file']# pandas无法保存布尔值,故简单对其进行一个判断# 对表中的数据转为小写,方便比对判断enable = i['enable']del i['enable']# 删除配置备份命令后的字典就是netmiko登录设备所需的信息dev = idev_infos.append((dev, config_file, enable))return dev_infosdef batch_config_push(inventory_file='config_inventory.xlsx'):push_results = []dev_infos = get_batch_config_push_dev_infos(inventory_file)for dev_info in dev_infos:# 我们使用一个星号* 后接一个元组或列表,就可以按顺序依次传入参数到函数中# 以下书写方式等同于network_device_config_push_by_config_file(dev_info[0],dev_info[1],dev_info[2])result = network_device_config_push_by_config_file(*dev_info)push_results.append(result)return push_resultsif __name__ == '__main__':results = batch_config_push()for i in results:print(i)

这段代码新增加了一个batch_config_push函数,它先初始化了一个用于存放推送结果的列表变量push_results。然后调用了get_batch_config_push_dev_infos函数加载了批量配置推送所需的信息,在for循环中依次实现每台设备的配置推送,并将结果追加到了之前初始的变量。

需要注意的点就是我们在调用network_device_config_push_by_config_file时,使用了一个星号*,将一个元组作为参数传入,它的效果是将元组成员依次展开传入函数当中,效果等同于network_device_config_push_by_config_file(dev_info[0],dev_info[1],dev_info[2]),我们用星号展开这种元组(列表也可)这种可迭代循环的对象,书写更加便捷。

其运行结果如下:

192.168.137.203配置推送发生异常,使用的配置文件为:192.168.137.203.config,异常信息:TCP connection to device failed.Common causes of this problem are:
1. Incorrect hostname or IP address.
2. Wrong TCP port.
3. Intermediate firewall blocking access.Device settings: cisco_ios 192.168.137.203:22{'push_sucess': True, 'session_log': '192.168.137.201.log'}
{'push_sucess': True, 'session_log': '192.168.137.202.log'}
{'push_sucess': False, 'session_log': '192.168.137.203.log'}

由于我们在每个设备推送配置的代码中有异常后会输出相关信息,且三台设备中有一台是不可达的,所以上面代码中有一台设备会在配置推送过程中报错,并在返回结果中返回相关信息。

配置推送小结

  1. netmiko的两个配置推送方法适合推送配置过程中无交互的场景。如果涉及到复杂交互,建议使用send_command。

  2. 这两个方法在开始的时候会自动进入配置模式,在输入完最后一条命令确认设备收到后会自动退出配置模式。

  3. 部分设备需要提权才可以进入配置模式,我们要根据情况合理调用enable方法或者使用send_command方法进行提权。

  4. 这两个方法只负责发送配置到设备,如果要生效,需要结合具体设备型号而言,有的在命令行中调用commit(比如华为的设备),有的可以调用commit方法。

  5. 配置的保存需要单独调用save_config方法。(极个别设备没有保存配置这一逻辑,或者是通过commit来实现配置生效,所以此方法调用的时候会报错)

  6. 配置推送相关脚本建议初始化连接的时候赋值session_log,用于了解和设备的整个交互过程,在程序失败的时候,可以查看log文件了解推送的情况,用于善后或者修正程序。

  7. 对待配置推送一定要有敬畏之心,建议从风险小,机械性高的场景开始。

  8. 配置推送一定要保证ssh隧道通常,如果是类似修改设备管理IP且立即生效,可能会导致程序失败。

3.6 新手使用Netmiko遇到的疑难杂症

新手在使用netmiko中总是会遇到形形色色、各式各样的问题,这些问题给我们带来了很多困惑。本章节我们将为大家讲解一下新手常遇到的问题以及解决方案。

3.6.1 开启netmiko的debug模式

在讲解问题之前,首先我们需要学会开启netmiko的debug模式,将netmiko底层运作的相关信息输出出来,同时也建议在初始化netmiko连接的同时将整个session进行日志记录。这两个方法有助于我们快速定位问题。如果开启debug模式,对于大家有困难,我们也可以选择后者,常见问题在netmiko的session日志当中也比较容易定位。

import logging
from netmiko import ConnectHandlerlogging.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: NInfo: 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 temporaryDEBUG: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 temporaryDEBUG:netmiko:Pattern found: screen\-length\ 0\ temporary screen-length 0 temporaryDEBUG:netmiko:screen-length 0 temporaryDEBUG: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 versionDEBUG:netmiko:Pattern found: display\ version display versionDEBUG: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 ConnectHandlerlogging.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-configurationDEBUG: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
#
aaalocal-user netdevops password irreversible-cipher $1c$G$bj3b+5YM$v(1\!wt&S,lN.YU7XLnM)`5RAa{2eNj~J]CiH1iD$local-user netdevops service-type sshlocal-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_commandraise 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 timefrom netmiko import ConnectHandlerdef 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.timeoutloop_delay = 0.2loops = timeout / loop_delayi = 0# 通过write_channel发送命令,命令后追加一个回车换行符conn.write_channel('{}{}'.format(cmd, conn.RETURN))# 进入循环,读取隧道中信息while i <= loops:# 读取隧道中的信息,放入chunk_outputchunk_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_outputbreak# 停顿loop_delay秒time.sleep(loop_delay)# 计数器i加一 类似其他语言中的i++i += 1# 如果超过了,则证明超时,我们可以自己抛出异常if i > loops:raise Exception('执行命令"{}"超时'.format(cmd))return outputif __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 timefrom netmiko import ConnectHandlerdev = {'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 + outputtime.sleep(loop_delay)i = i + 1if 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 ConnectHandlerdev = {'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 timedev = {'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经常会遇到配置不全的情况,产生这种问题有两种原因:

  1. 执行命令耗时较长,在timeout规定的时间窗口内未收集全配置,send_command_timing不再读取隧道中信息,强制退出,将已收集到的回显返回。
  2. 设备在传输回显的过程中,会稍微“卡顿”几秒,比如思科的设备在查看配置的时候,会在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 logginglogging.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 timedev = {'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 redev = {'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_outputchunk_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_outputbreak# 停顿loop_delay秒time.sleep(loop_delay)# 计数器i加一 类似其他语言中的i++i += 1# 如果超过了,则证明超时,我们可以自己抛出异常if i > loops:raise Exception('执行命令"{}"超时'.format(cmd))print(output)

这段代码,我们结合了之前取消分页权限不足的相关逻辑。只需要修改一些变量,能达到比较通用的效果。但是在实际使用中还是要结合场景及交互可能出现的情况,甚至是意外情况等在合适的位置对回显进行相关的判断与确认。上述回显的时候,会把我们输入的命令也输出出来,如果在做配置备份等场景,想去掉这个发送的命令,我们可以在前面读取的时候用空字符串替换掉。

这段代码也可以进一步封装成函数,方便我们重复使用。

Netmiko终极宝典相关推荐

  1. Sublime Text3终极宝典

    众所周知Sublime Text3是一款特别高效快捷的开发工具,深受开发人员的喜爱,那么作为程序员的我们一定要对Sublime Text3的快捷键有深入的理解和掌握.只有这样我们才能快速高效的进行编程 ...

  2. C语言终极宝典微盘,C语言终极面试宝典.pdf

    1912 制作 第一部分:基本概念及其它问答题 1.关键字static 的作用是什么? 这个简单的问题很少有人能回答完全.在C 语言中,关键字static 有三个明显的作用: 1). 在函数体,一个被 ...

  3. 剑网三修复选择服务器,老玩家回坑剑网三去哪个区服,要注意啥?这有一份终极宝典请查收...

    常常有人问,三四年没玩剑网三了,想回坑要选哪个区,哪个门派,要注意些什么比较好? 所以今天咱们就从最基础的建号开始讲讲新人入坑.老人回坑有哪些需要注意的,帮助想要开始游戏的玩家们解答一些疑惑. 关于服 ...

  4. HTC手机鉴别终极宝典

    1.获取手机的S/N码 在手机上操作:设置--关于手机--设备信息(设备序列号,即是我们要找的S/N码,可以先记下来存在一个记事本中) 查询S/N码方法2: 查询销售地 出厂日期   ------- ...

  5. Java多线程与并发编程终极宝典

    阅读本文需要了解的概念 原语 所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断.在操作系统中,某些被进程调用的操作,如队列操作.对信号量的操作.检查启动外设操作 ...

  6. 七夕祸害情侣终极宝典

    1,打电话把酒店大床房预订光. 2,上街扇别人男友耳光. 2,去影院把单号座买光. 3,拿小针去超市把TT都扎光. 4,上街卖花,见情侣就说:给你妈买束花吧! 5,吃饭不结账,走时指着别人男友说:我前 ...

  7. 面试终极宝典:Springboot自动装配原理

    Springboot的自动装配过程,网上大多都是罗列代码,即使看完了,也还存在很多的疑点.下面,这是我总结的在面试过程中介绍SpringBoot自动装配原理的话术,拿来即用就可. Springboot ...

  8. 新手终极宝典nbsp;iTunes使用教程精华大全

    <script language='javascript' src='http://www.shiqiaotou.com/donetk/Header.js'></script> ...

  9. 彻底搞懂 Scrapy 的中间件

    彻底搞懂Scrapy的中间件(一):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_1.html 彻底搞懂Scrapy的中 ...

最新文章

  1. 数据统计之日活跃用户统计
  2. 如何根据值查找所在的表和字段
  3. catch后面的代码会执行吗_字节码层面理解try、catch、finally
  4. 【AutoML】损失函数也可以进行自动搜索学习吗?
  5. Windows平台九点提升权限终极技巧
  6. 轻量又高效,Apache Shiro 你值得拥有!
  7. WPF之Binding(转)
  8. 洛谷 P1129 [ZJOI2007]矩阵游戏 解题报告
  9. Flex4之皮肤定制
  10. Android开发笔记(八十二)SDK版本兼容
  11. 发那科oimf是什么时候出的_请问下FANUC Oi-MF系统怎么把卡里的程序传到机床里面?...
  12. 网页添加QQ/MSN链接
  13. 初级网优工程师需要符合什么标准?华为初级认证重点知识!
  14. vue中使用leaflet加载地图影像并拾取坐标点
  15. 计算机导论——计算机软件03
  16. iOS依赖注入框架系列(二):设置Typhoon
  17. Windows服务器搭建Node-Media-Server视频服务器
  18. C++ vector 中sort的一些用法
  19. 【IMRaD】如何“科学地”写一篇科学研究论文
  20. oracle dump 文件解析,实用解析dmp文件内容

热门文章

  1. CS不出网各种上线姿势
  2. python人才_【北软互联】上海python人才外派|上海python程序员人力外包|上海python工程师开发驻场|上海python软件劳务派遣公司...
  3. owmctab.plb oracle,ORA-00904: WMSYS.WM_CONCAT: invalid identifier
  4. dpsk调制解调 matlab,2DPSK调制与解调系统的MATLAB实现及性能分析.doc
  5. Permission权限管理——隐私权限具体说明
  6. Office2007如何将Word文档转换PDF文档
  7. 支付宝网站支付demo运行教程
  8. 高德地图 下拉搜索五省 根据电价区分区域颜色
  9. 简单易懂的“测试计划”模板
  10. android课程表删除课程,超级课程表话题删除方法