1. 背景介紹
曾經調試過千臺設備,也敲過萬行代碼,當年無論大小割接,絕不提前準備腳本,都是現場憑藉比跳egg頻率還高的手速敲擊命令;最近發現隨着年紀的增加越來越不想幹活,哪怕修改一臺設備接口描述也會將配置在txt中寫好,再粘貼下發。遂在閒暇之餘研究起了網絡自動化。網絡自動化沒有一個具體的概念,他是一個體系框架,在這框架內可以極高的解放生產力,淘汰CCNA Level Engineer。換句話說可以實現高頻率重複性無腦操作的自動化。但正正意義上的網絡自動化內容遠不止於此;本系列文章所涉及的網絡自動化,均是將高頻率重複性無腦操作的自動化視爲網絡自動化。本人學藝不精,不能通過一篇文章全面的描述NAPALM的使用,需要不斷學習、吸收再分享,故本文爲系列性文章,不定期更新。
下面我先簡單介紹下NAPALM應用場景:
首先,讓我們假想一個場景:
由於業務發生變更,需要爲一個 POD 裏面的幾十臺交換機修改 QoS 配置。作爲網絡運維人員,應該怎樣處理這項工作呢?
如果需要變更的對象是整個數據中心幾百臺甚至幾千臺交換機,又該怎樣處理這項工作呢?
當下,互聯網行業已經普遍採用 DevOps 的體系流程。靠人力去一臺設備一臺設備的更改配置,已經不再是正確的思維方式。原因不僅僅是浪費時間 —— 要知道,人如果要長時間保持注意力集中,大腦需要耗費大量的能量,很難保證不出現遺漏或者錯誤。而機器卻不會。
因此,正確的方法是利用 DevOps 的流程,讓機器來完成這項工作。例如採用基於 Python 的 SSH 庫 Paramiko 或 Netmiko,以及 Ansible 或 SaltStack 等自動化工具編寫運維腳本。
Netmiko 庫和 Ansible 等運維工具雖然可以通過程序化的腳本對網絡設備實現批量管理,但仍然需要運維工程師對網絡設備的 CLI 很熟悉,預先在腳本中建立需要被執行的 Command 列表。因此運維人員離不開以下兩個體系分別是CLI和SNMP協議:
- CLI:命令行工具,不同廠商之間、同廠商不同版本、不同設備之間命令語法均可能不一樣;這造成運維工程師學習成本的增加。同時設備版本升級後,配置也有不兼容的情況。
- SNMP:SNMP Agent 會維護一個 MIB(管理信息庫),裏面保存着大量的 OID (對象標識符)。一個 OID 是一對唯一的 Key-Value,SNMP Manager 向 SNMP Agent 查詢或修改若干 Key 所對應的 Value,就可以實現信息採集或者網絡設備的配置修改。SNMP也存在以下問題:
1. 太古老,性能不好。基於 UDP 協議傳輸,比較不可靠。雖然在應用層有 Response 機制保證丟包之後的重複 get/ set,但代價就是性能和運行時間都受到影響
2. 致命的問題是,各廠商都大量的使用私有 MIB,卻不存在一個可以自動發現網絡設備當前所採用的 MIB 的機制。網絡運維人員必須分別向設備廠商索取網絡設備的 MIB,耗費大量的時間整理自己需要的 OID,再手工導入到自動化運維平臺或者腳本當中。
所以 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支持很好。下面我們將進入是實驗部分。
2. 實驗目的
- 安裝NAPALM
- 通過NAPALM登錄設備
- 通過NAPALM自定義函數方法
3. 物料清單
- EVE-GN 網絡模擬器
- PC1臺
- Python3.7
- NAPALM
4. 實驗拓撲
在EVE_GN中新建一臺IOS交換機,然後橋接到PC網卡(任意能與物理PC通信的網卡),本實驗環境是橋接到vm station的NAT網卡。IP地址如下圖:
5. 實驗記錄
- 安裝NAPALM
本人PC系統爲Win10 64bit enterprise,python版本如下3.7.3:
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
- 通過NAPALM實現TELNET連接設備:圖中交換機IP地址爲192.168.162.111,但是我們現在將使用EVE_NG服務器IP+設備端口號的形式來登錄設備,即: 192.168.162.133 32778 來登錄設備;因爲EVE_NG默認控制檯putty是使用上述IP信息和telnet協議;後面將介紹如何使用SSH連接設備。
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
運行和預期一樣,正常工作。