曾经调试过千台设备,也敲过万行代码,当年无论大小割接,绝不提前准备脚本,都是现场凭借比跳egg频率还高的手速敲击命令;最近发现随着年纪的增加越来越不想干活,哪怕修改一台设备接口描述也会将配置在txt中写好,再粘贴下发。遂在闲暇之余研究起了网络自动化。网络自动化没有一个具体的概念,他是一个体系框架,在这框架内可以极高的解放生产力,淘汰CCNA Level Engineer。换句话说可以实现高频率重复性无脑操作的自动化。但正正意义上的网络自动化内容远不止于此;本系列文章所涉及的网络自动化,均是将高频率重复性无脑操作的自动化视为网络自动化。本人学艺不精,不能通过一篇文章全面的描述NAPALM的使用,需要不断学习、吸收再分享,故本文为系列性文章,不定期更新。
下面我先简单介绍下NAPALM应用场景:
首先,让我们假想一个场景:
由于业务发生变更,需要为一个 POD 里面的几十台交换机修改 QoS 配置。作为网络运维人员,应该怎样处理这项工作呢?
如果需要变更的对象是整个数据中心几百台甚至几千台交换机,又该怎样处理这项工作呢?
当下,互联网行业已经普遍采用 DevOps 的体系流程。靠人力去一台设备一台设备的更改配置,已经不再是正确的思维方式。原因不仅仅是浪费时间 —— 要知道,人如果要长时间保持注意力集中,大脑需要耗费大量的能量,很难保证不出现遗漏或者错误。而机器却不会。
因此,正确的方法是利用 DevOps 的流程,让机器来完成这项工作。例如采用基于 Python 的 SSH 库 Paramiko 或 Netmiko,以及 Ansible 或 SaltStack 等自动化工具编写运维脚本。
Netmiko 库和 Ansible 等运维工具虽然可以通过程序化的脚本对网络设备实现批量管理,但仍然需要运维工程师对网络设备的 CLI 很熟悉,预先在脚本中建立需要被执行的 Command 列表。因此运维人员离不开以下两个体系分别是CLI和SNMP协议:
所以 SNMP 和CLI只适合用来做信息采集,提供告警和可视化报表,但自动化运维的 API 则需要考虑其他的选项。站在网络运维人员的角度,这个 API 到底什么样的?其应该具有以下特点:
容易使用 —— Usability 是所有产品的核心价值
需要能够清晰地区分“配置数据”,“设备运行状态数据”和“统计数据”
需要能够分别从各个网络设备获取上述 3 种数据,并且可以方便地对比不同设备的数据
可以让网络运维人员统一地管理整个网络的所有设备,而不是一台一台的单独管理
对不同厂商的设备都能够使用同一种配置方法
配置变更对网络业务的影响要尽可能的小
能够提供一个标准化的,对设备 Pulling 和 Pushing 配置文件的流程,以满足对设备配置的备份和恢复的业务需求
能够很方便地,持续地,检查设备配置文件的一致性
能够提供基于文本的配置方式,并且不会导致配置的乱序,例如不能搅乱 ACL 规则的顺序
目前能够满足这些要求的网络设备的北向 API 接口就是 Netconf,但是于很多厂商虽然支持 Netconf,但有一些 Key-Value 却存在差异。比如为了表达“端口”,有些厂商用 intf 作为 Key,但另外一些厂商却用 interface 作为 Key。另一个例子就是 Uptime,设备运行时间,各家厂商的设备返回的时间格式更是五花八门。这为网络运维人员处理数据的工作造成了很大的麻烦,不得不耗费大量的时间和精力去阅读设备厂商的 Netconf 文档,去编写大量的正则表达式。
还有,虽然主流的 SDN Controller 的南向接口都支持 Netconf,但是在实际部署时,却无法用单一的 Controller 去控制多厂商的网络设备。通常都是各个厂商使用自己的 SDN Controller 控制自己的设备,然后再用 REST API 与用户的 SDN Controller 对接。故NAPALM出现在我们面前:
NAPALM 是一个 Python 库,它的全称是 Network Automation and Programmability Abstraction Layer with Multivendor support,多厂商支持的网络自动化和可编程抽象层。
目前 Ansible 集成了 3 个 NAPALM 模块,分别是:
napalm_parse_yang:用于从设备或文件中解析配置/状态数据
napalm_diff_yang:用于比较 2 个 YANG 对象的差异
napalm_translate_yang:用于将 YANG 对象转译成设备原始的配置
从设备取出原始配置数据/状态数据之后,可以使用 NAPALM 将其翻译成标准格式的 NAPALM 数据。反之,也可以将标准格式的 NAPALM 数据翻译成设备原始配置数据,并 Push 到网络设备里面,以修改设备的配置文件。
是的,NAPALM 还是不能彻底解决网络自动化所面临的问题。
因为各厂商 Netconf 的数据表达存在很多差异,所以 NAPALM 必须要依赖第三方的 Module 来完成原始数据的解析和翻译。如果要解析厂商 A 的某个 OS 系统的配置,就需要一个 OSA_Module;如果要解析厂商 B 的某个 OS 系统的配置,则需要 OSB_Module。所以目前 NAPALM 支持的 OS 类型还比较少,仅限于某几个国外品牌厂商的 OS 系统。
但是NAPALM是目前被集成最广泛的网络自动化库,对思科和juniper支持很好。下面我们将进入是实验部分。
在EVE_GN中新建一台IOS交换机,然后桥接到PC网卡(任意能与物理PC通信的网卡),本实验环境是桥接到vm station的NAT网卡。IP地址如下图:
C:\Users\Nero>python --version
Python 3.7.3
C:\Users\Nero>systeminfo
主机名: DESKTOP-KDNS6Q5
OS 名称: Microsoft Windows 10 企业版
OS 版本: 10.0.17763 暂缺 Build 17763
OS 制造商: Microsoft Corporation
....
系统类型: x64-based PC
在cmd输入pip install napalm
将自动安装NAPALM及其依赖包。
pip install napalm #安装napalm
pip install napalm -U #更新napalm
from napalm import get_network_driver #导入相关模块
driver = get_network_driver('ios') #指定设备类型为ios,NAPALM对设备的支持是安装版本来实现的,在登录设备之前必须先指定NOS类型
device = driver(
'192.168.162.133', #设备ip地址
None, #设备用户名,此设备未设置用户名
None, #密码,此设备未设置密码
optional_args={'port':32778,'transport':'telnet','secret':'enable123'} #选项字段,指定端口协议enable密码, NAPALM默认为SSH端口号22.
)
device.open() # 配置好以后,连接设备,没报错即连接成功
device.load_merge_candidate(config='hostname test_device') #下发配置 hostname test_device修改主机名; 这里需要在交换机上开始SCP,如果没开启,NAPALM会报错提醒。
print(device.compare_config(),"step=1") #比较配置信息,详见后文脚本运行日志
device.commit_config() #确认下发
print(device.compare_config(),"step=2") #为了对比,再比对一次配置,应没有新增配置,详见后文脚本运行日志
device.rollback() #配置回滚(回退),结果应该是hostname回复为默认 switch
device.close() #断开连接
脚本运行打印日志:
+hostname test_device step=1 #上面比较配置信息显示新增该配置(step=1 是为了方便观察程序运行添加,不影响功能)
step=2 #配置commit后再比较配置,显示未新增配置,符合预期。
通过在交换机控制台观察到现象,注意设备名由Switch—>test_device------Switch的变化,因为上面的配置将设备名修改为test_device后,再进行配置回滚,恢复到Switch:
3. 通过SSH连接设备:事先配置好G1/0接口,并给交换机配置好IP地址为192.168.162.111.配置好用户名密码。
from napalm import get_network_driver
driver = get_network_driver('ios')
with driver('192.168.162.111','napalm','napalm',optional_args={'port':22},) as device: #通过with打开连接设备。默认为ssh连接,无须额外配置
print(device.get_facts())
运行上面的脚本输出如下,返回的是一个字典,内容有运行时间uptime、厂商vendor、版本os_version,可以看书NAPALM已经将设备回显给标准化了,无论什么厂商的设备都将显示为如下格式:
{'uptime': 2280, 'vendor': 'Cisco', 'os_version': 'vios_l2 Software (vios_l2-ADVENTERPRISEK9-M), Version 15.2(4.0.55)E, TEST ENGINEERING ESTG_WEEKLY BUILD, synced to END_OF_FLO_ISP', 'serial_number': '9YHU26C8R49', 'model': 'IOSv', 'hostname': 'Switch', 'fqdn': 'Switch.ivi', 'interface_list': ['GigabitEthernet0/0', 'GigabitEthernet0/1', 'GigabitEthernet0/2', 'GigabitEthernet0/3', 'GigabitEthernet1/0', 'GigabitEthernet1/1', 'GigabitEthernet1/2', 'GigabitEthernet1/3', 'GigabitEthernet2/0', 'GigabitEthernet2/1', 'GigabitEthernet2/2', 'GigabitEthernet2/3', 'GigabitEthernet3/0', 'GigabitEthernet3/1', 'GigabitEthernet3/2', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'Vlan1', 'Vlan300']}
自定义方法:虽然NAPALM有很多内建方法可以得到常用网络信息如使用get_mac_table()得到mac地址表、get_interfaces_ip()得到ip信息等,但是实际网络环境复杂多样,总会遇到有需要使用额外命令的时候,下面我们假设需要设备输出show ip interface brief信息。对应的解决方案是扩展NAPALM库,新增一个自定义方法,完成输出格式化,下次再使用的时候直接调用即可。新增该方法有两个途径
途径一是通过在在NAPALM对应的路径下的模块中直接增加自定义的函数,然后关闭,再调用。下面我们来操作一把:
我的路径是C:\Program Files\Python37\Lib\site-packages\napalm
,找到其中的ios文件夹:
下图中ios.py 即是ios设备的库文件,所有方法均在该文件中,因此我们可以打开它直接在其后面新增自己编写的方法。
我们编写一个脚本用于输出show ip interface brief
的显示
def cuget_interfaces_brief(self):
command = 'show ip interface brief'
output = self._send_command(command)
return_vars = []
for line in output.splitlines():
return_vars.append(tuple(line.split()))
return return_vars #将返回一个列表,列表内嵌套元组,命令show ip interface brief 在屏幕上的回显一行放进一个元组
我们将上述脚本直接粘贴到ios.py
文件的末尾,然后保存:
现在我们连接设备后就可以使用该方法了:
from napalm import get_network_driver
driver = get_network_driver('ios')
with driver('192.168.162.111','napalm','napalm',optional_args={'port':22},) as device: #通过with方法连接设备,少了device.open()这一步
# device.load_merge_candidate(config='hostname Switch')
print(device.get_facts())
print(device.cuget_interfaces_brief()) #显示自定义方法输出结果
#为了让输出结果和show ip interface brief类似,我们下面对自定义方法进行格式化输出
t = 0
for i in device.cuget_interfaces_brief():
for tup in i:
print(tup,end='\t'*3) if t == 0 else print(tup,end='\t'*2)
print()
t += 1
脚本输出如下:
print(device.get_facts())
的输出:
{'uptime': 2280, 'vendor': 'Cisco', 'os_version': 'vios_l2 Software (vios_l2-ADVENTERPRISEK9-M), Version 15.2(4.0.55)E, TEST ENGINEERING ESTG_WEEKLY BUILD, synced to END_OF_FLO_ISP', 'serial_number': '9YHU26C8R49', 'model': 'IOSv', 'hostname': 'Switch', 'fqdn': 'Switch.ivi', 'interface_list': ['GigabitEthernet0/0', 'GigabitEthernet0/1', 'GigabitEthernet0/2', 'GigabitEthernet0/3', 'GigabitEthernet1/0', 'GigabitEthernet1/1', 'GigabitEthernet1/2', 'GigabitEthernet1/3', 'GigabitEthernet2/0', 'GigabitEthernet2/1', 'GigabitEthernet2/2', 'GigabitEthernet2/3', 'GigabitEthernet3/0', 'GigabitEthernet3/1', 'GigabitEthernet3/2', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'GigabitEthernet3/3', 'Vlan1', 'Vlan300']}
print(device.cuget_interfaces_brief())
的输出
[('Interface', 'IP-Address', 'OK?', 'Method', 'Status', 'Protocol'), ('GigabitEthernet0/0', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet0/1', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet0/2', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet0/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet1/0', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet1/1', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet1/2', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet1/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet2/0', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet2/1', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet2/2', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet2/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/0', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/1', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/2', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('GigabitEthernet3/3', 'unassigned', 'YES', 'unset', 'up', 'up'), ('Vlan1', 'unassigned', 'YES', 'unset', 'administratively', 'down', 'down'), ('Vlan300', '192.168.162.111', 'YES', 'NVRAM', 'up', 'up')]
格式化print(device.cuget_interfaces_brief())
后的输出:是不是和CLI 屏幕输出一样了? 爽不爽
Interface IP-Address OK? Method Status Protocol
GigabitEthernet0/0 unassigned YES unset up up
GigabitEthernet0/1 unassigned YES unset up up
GigabitEthernet0/2 unassigned YES unset up up
GigabitEthernet0/3 unassigned YES unset up up
GigabitEthernet1/0 unassigned YES unset up up
GigabitEthernet1/1 unassigned YES unset up up
GigabitEthernet1/2 unassigned YES unset up up
GigabitEthernet1/3 unassigned YES unset up up
GigabitEthernet2/0 unassigned YES unset up up
GigabitEthernet2/1 unassigned YES unset up up
GigabitEthernet2/2 unassigned YES unset up up
GigabitEthernet2/3 unassigned YES unset up up
GigabitEthernet3/0 unassigned YES unset up up
GigabitEthernet3/1 unassigned YES unset up up
GigabitEthernet3/2 unassigned YES unset up up
GigabitEthernet3/3 unassigned YES unset up up
GigabitEthernet3/3 unassigned YES unset up up
GigabitEthernet3/3 unassigned YES unset up up
GigabitEthernet3/3 unassigned YES unset up up
GigabitEthernet3/3 unassigned YES unset up up
Vlan1 unassigned YES unset administratively down down
Vlan300 192.168.162.111 YES NVRAM up up
***Repl Closed***
途径二:扩展NAPALM的第二种方法是新增配置文件,新建类方法,然后继承对应版本的内建class,既可以使用自建类,也可以继承内建类的所有方法。这种方法不修改和影响内建方法,灵活性更强,推荐使用该方法:
首先在 C:\Program Files\Python37\Lib\site-packages\napalm
下新建文件夹,取名custom_ios
:
文件夹内新建两个文件__init__.py
和ios.py
,前者的名字必须固定为__init__.py
:
分别在两个文件粘贴如下内容:
__init__.py
:内容如下
"""custom.napalm.ios package.by nero"""
from napalm.custom_ios.ios import CustomIOSDriver
__all__ = ["CustomIOSDriver"]
ios.py
内容如下:
from napalm.ios.ios import IOSDriver
class CustomIOSDriver(IOSDriver): #注意__init__.py中的对应名字一定要和这里class的名字保持一致,同时这里继承了内建IOSDriver的所有方法,便于我们调用自定义方法的时候可以继续使用内建的方法
"""Custom NAPALM Cisco IOS Handler."""
def cuget_interfaces_brief(self):
command = 'show ip interface brief'
output = self._send_command(command)
return_vars = []
for line in output.splitlines():
return_vars.append(tuple(line.split()))
return return_vars
完成以上操作后就可以开始测试效果了:将之前的例子调用方法修改一下,其他语句均不变,如下:
from napalm import get_network_driver
driver = get_network_driver('custom_ios') #括号内是刚才新建文件夹的名字,仅修改此处
with driver('192.168.162.111','napalm','napalm',optional_args={'port':22},) as device:
print(device.get_facts())
print(device.cuget_interfaces_brief())
t = 0
for i in device.cuget_interfaces_brief():
for tup in i:
print(tup,end='\t'*3) if t == 0 else print(tup,end='\t'*2)
print()
t += 1
运行和预期一样,正常工作。
- http://www.ruijie.com.cn/fa/xw-hlw/61232/
- NAPALM’s documentation