Cisco 交換機利用CDP資料自動繪製網路拓撲圖[drawio]-實踐

顏天高佑發表於2023-12-25

進行網路運維,必須對網路拓撲情況進行詳細的掌握,但是網路改動後,更新網路拓撲比較繁瑣,維護人員容易懈怠,久而久之,透過人工繪製的網路拓撲很容易與現有網路出現偏差。

現在,可以透過python 豐富的庫,結合CDP鄰居資訊,自動繪製網路拓撲資訊,以下是實現思路:

1、登入裝置,獲取鄰居資訊;

  工具:python(telnetlib、paramiko、netmiko庫)

2、篩選需要的資訊,裝置本地IP,本地名稱,本地介面,對端裝置名稱,對端介面,對端IP

  工具:python(textfsm、json庫)

3、根據獲取進行進行畫圖

  工具:python(N2G庫)

  文件說明:https://n2g.readthedocs.io/en/latest/diagram_plugins/DrawIo%20Module.html

4、調整圖形。

 

一、透過python登入交換機裝置的案例很多,在此不再贅述,各位可以在網上找到透過telnet、ssh的方式登入交換機,根據實際情況進行調整;這裡後續透過telnet方式登入裝置,輸入show cdp nei detali 獲取資訊。

二、鄰居資訊處理

  2.1獲取鄰居資訊

    輸入命令後,獲取交換機鄰居資訊如下:


 1 QIA.JSJZX.JKS>show cdp nei
 2 -------------------------
 3 Device ID: qia.b3.net.test.sw01
 4 Entry address(es):
 5   IP address: 192.17.190.225
 6 Platform: cisco WS-C2960X-24TS-L,  Capabilities: Switch IGMP
 7 Interface: GigabitEthernet1/0/24,  Port ID (outgoing port): GigabitEthernet1/0/23
 8 Holdtime : 126 sec
 9 
10 Version :
11 Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E6, RELEASE SOFTWARE (fc1)
12 Technical Support: http://www.cisco.com/techsupport
13 Copyright (c) 1986-2016 by Cisco Systems, Inc.
14 Compiled Fri 16-Dec-16 21:27 by prod_rel_team
15 
16 advertisement version: 2
17 Protocol Hello:  OUI=0x00000C, Protocol ID=0x0112; payload len=27, value=00000000FFFFFFFF010221FF000000000000F87B20311580FF0000
18 VTP Management Domain: ''
19 Native VLAN: 1
20 Duplex: full
21 Power Available TLV:
22 
23     Power request id: 0, Power management id: 1, Power available: 0, Power management level: -1
24 Management address(es):
25   IP address: 192.17.190.225
26 
27 -------------------------
28 Device ID: qia.b3.net.test.sw03
29 Entry address(es):
30   IP address: 192.17.191.132
31 Platform: cisco WS-C2960S-48TD-L,  Capabilities: Switch IGMP
32 Interface: GigabitEthernet1/0/23,  Port ID (outgoing port): GigabitEthernet1/0/48
33 Holdtime : 134 sec
34 
35 Version :
36 Cisco IOS Software, C2960S Software (C2960S-UNIVERSALK9-M), Version 12.2(55)SE7, RELEASE SOFTWARE (fc1)
37 Technical Support: http://www.cisco.com/techsupport
38 Copyright (c) 1986-2013 by Cisco Systems, Inc.
39 Compiled Mon 28-Jan-13 10:28 by prod_rel_team
40 
41 advertisement version: 2
42 Protocol Hello:  OUI=0x00000C, Protocol ID=0x0112; payload len=27, value=00000000FFFFFFFF010221FF000000000000B000B4865F80FF0000
43 VTP Management Domain: 'default'
44 Native VLAN: 1
45 Duplex: full
46 Power Available TLV:
47 
48     Power request id: 0, Power management id: 1, Power available: 0, Power management level: -1
49 Management address(es):
50   IP address: 192.19.191.132

  上述為鄰居資訊欄位,標紅部分為需要提取的資訊內容,下面透過textfsm工具進行提取,獲取到資訊內容分別為:本機名稱,鄰居主機名稱、鄰居主機IP、本機介面、鄰居介面;以下是自定義的textfsm模板,檔案儲存為cisco_tfm.template。

 1 Value Local_hostname (\S+)
 2 Value Key Local_port (\S+)
 3 Value Device_name (\S+)
 4 Value Device_module (\S+)
 5 Value Device_IP (\S+)
 6 Value Required Device_port (\S+)
 7 
 8 
 9 Start
10  ^${Local_hostname}>
11  ^Device ID: ${Device_name}
12  ^\s+IP\saddress: ${Device_IP}
13  ^Platform: cisco ${Device_module},
14  ^Interface: ${Local_port},\s+Port\sID\s\(outgoing\sport\):\s${Device_port} -> Record

 程式碼實現:

 1 import textfsm
 2 
 3 
 4 #cisco :show cdp nei detail
 5 data = """
 6 QIA.JSJZX.JKS>show cdp nei
 7 -------------------------
 8 Device ID: qia.b3.net.test.sw01
 9 Entry address(es):
10   IP address: 192.17.190.225
11 Platform: cisco WS-C2960X-24TS-L,  Capabilities: Switch IGMP
12 Interface: GigabitEthernet1/0/24,  Port ID (outgoing port): GigabitEthernet1/0/23
13 Holdtime : 126 sec
14 
15 Version :
16 Cisco IOS Software, C2960X Software (C2960X-UNIVERSALK9-M), Version 15.2(2)E6, RELEASE SOFTWARE (fc1)
17 Technical Support: http://www.cisco.com/techsupport
18 Copyright (c) 1986-2016 by Cisco Systems, Inc.
19 Compiled Fri 16-Dec-16 21:27 by prod_rel_team
20 
21 advertisement version: 2
22 Protocol Hello:  OUI=0x00000C, Protocol ID=0x0112; payload len=27, value=00000000FFFFFFFF010221FF000000000000F87B20311580FF0000
23 VTP Management Domain: ''
24 Native VLAN: 1
25 Duplex: full
26 Power Available TLV:
27 
28     Power request id: 0, Power management id: 1, Power available: 0, Power management level: -1
29 Management address(es):
30   IP address: 192.17.190.225
31 
32 -------------------------
33 Device ID: qia.b3.net.test.sw03
34 Entry address(es):
35   IP address: 192.17.191.132
36 Platform: cisco WS-C2960S-48TD-L,  Capabilities: Switch IGMP
37 Interface: GigabitEthernet1/0/23,  Port ID (outgoing port): GigabitEthernet1/0/48
38 Holdtime : 134 sec
39 
40 Version :
41 Cisco IOS Software, C2960S Software (C2960S-UNIVERSALK9-M), Version 12.2(55)SE7, RELEASE SOFTWARE (fc1)
42 Technical Support: http://www.cisco.com/techsupport
43 Copyright (c) 1986-2013 by Cisco Systems, Inc.
44 Compiled Mon 28-Jan-13 10:28 by prod_rel_team
45 
46 advertisement version: 2
47 Protocol Hello:  OUI=0x00000C, Protocol ID=0x0112; payload len=27, value=00000000FFFFFFFF010221FF000000000000B000B4865F80FF0000
48 VTP Management Domain: 'default'
49 Native VLAN: 1
50 Duplex: full
51 Power Available TLV:
52 
53     Power request id: 0, Power management id: 1, Power available: 0, Power management level: -1
54 Management address(es):
55   IP address: 192.17.191.132
56 """
57 
58 template_file = ".\cisco_tfm.template"
59 
60 with open(template_file) as template:
61     fsm = textfsm.TextFSM(template)
62     result = fsm.ParseText(data)
63     # print(fsm.header)
64     print(result)
65     # print(len(result))

輸出內容:

['Local_hostname', 'Local_port', 'Device_name', 'Device_module', 'Device_IP', 'Device_port']
[['QIA.JSJZX.JKS', 'GigabitEthernet1/0/24', 'qia.b3.net.test.sw01', 'WS-C2960X-24TS-L', '192.17.190.225', 'GigabitEthernet1/0/23'], 
['', 'GigabitEthernet1/0/23', 'qia.b3.net.test.sw03', 'WS-C2960S-48TD-L', '192.17.191.132', 'GigabitEthernet1/0/48']]

注意:以上內容中,本地主機名僅在第一個鄰居資訊表中顯示,所以需要當前裝置鄰居進行資料進行格式化,因此就需要用到json庫。

2.2 鄰居資訊資料格式化

透過資料格式轉換,可以得到當前主機IP、主機名稱、主機介面;鄰居IP、鄰居名稱、鄰居介面資訊;

程式碼實現:

 

Cisco 交換機利用CDP資料自動繪製網路拓撲圖[drawio]-實踐
 1 import json
 2 
 3 ip_address = '192.168.1.1'
 4 # hostname = 'zh_cisco_2960'
 5 
 6 cdp_data = [
 7     ['9QI.JSJZX.JKS', 'GigabitEthernet1/0/24', '9qi.b3.net.test.sw01', 'WS-C2960X-24TS-L', '172.17.190.225', 'GigabitEthernet1/0/23'],
 8     ['', 'GigabitEthernet1/0/23', '9qi.b3.net.test.sw03', 'WS-C2960S-48TD-L', '172.17.191.132', 'GigabitEthernet1/0/48']
 9 ]
10 
11 def data_format(ip_address,cdp_data):
12     
13     hostname = cdp_data[0][0]#二層列表格式,獲取主機名
14     result_data = {
15         ip_address: {
16             hostname: {item[1]: item[2:] for item in cdp_data}
17         }
18     }
19     json_data = json.dumps(result_data, indent=2)
20 
21     return json_data
22 # return the JSON formatted string
23 
24 if __name__ == "__main__":
25     json_data1 = data_format(ip_address,cdp_data)
26     print(json_data1)
Json_CDPInfo

 輸出資訊:

Cisco 交換機利用CDP資料自動繪製網路拓撲圖[drawio]-實踐
 1 {
 2   "192.168.1.1": {#當前裝置IP
 3     "QI.JSJZX.JKS": {#當前裝置名稱
 4       "GigabitEthernet1/0/24": [#本地介面,
 5         "qia.b3.net.test.sw01",#鄰居資訊
 6         "WS-C2960X-24TS-L",
 7         "192.17.190.225",
 8         "GigabitEthernet1/0/23"
 9       ],
10       "GigabitEthernet1/0/23": [
11         "qia.b3.net.test.sw03",
12         "WS-C2960S-48TD-L",
13         "192.17.191.132",
14         "GigabitEthernet1/0/48"
15       ]
16     }
17   }
18 }
View Code

格式化以上鄰居資料後,便於後續對資料進行遍歷,在第三步進行讀取資料進行增加節點。

遍歷資料:

Cisco 交換機利用CDP資料自動繪製網路拓撲圖[drawio]-實踐
 1 import json
 2 
 3 # 讀取 JSON 資料
 4 json_data = '''
 5 {
 6   "192.168.1.1": {
 7     "QIA.JSJZX.JKS": {
 8       "GigabitEthernet1/0/24": [
 9         "qia.b3.net.test.sw01",
10         "WS-C2960X-24TS-L",
11         "192.17.190.225",
12         "GigabitEthernet1/0/23"
13       ],
14       "GigabitEthernet1/0/23": [
15         "qia.b3.net.test.sw03",
16         "WS-C2960S-48TD-L",
17         "192.17.191.132",
18         "GigabitEthernet1/0/48"
19       ]
20     }
21   }
22 }
23 
24 '''
25 
26 # 解析 JSON 資料
27 parsed_data = json.loads(json_data)
28 
29 # 遍歷資料
30 for ip_address, inner_data in parsed_data.items():
31     print(f"IP Address: {ip_address}")# 獲取主機IP地址
32     # print(f"host: {inner_data}")
33 
34     for hostname, cdp_data in inner_data.items():
35       print(f"Hostname: {hostname}") #獲取主機名稱
36       # print(type(cdp_data))
37 
38       for key, values in cdp_data.items():
39         print(values)
View Code

這裡在每個節點獲取到鄰居資訊,就可以根據資訊,在拓撲圖中增加節點資訊了。

三、進行畫圖、

3.1 小試牛刀

網路拓撲圖中, 最重要的資訊就是節點和互聯線路,其他都為輔助資訊

在官方文件中,已經有詳細的說明可以增加節點(addnode)、增加連線(addlink);在這個兩個功能中,還有其他的選項,可以補充增加,官方提供的方法,可以透過help 檢視文件說明help(N2G.plugins.diagrams.N2G_DrawIO.drawio_diagram)

這裡只需要重點檢視Quick start部分增加節點和連線就可以了。

1 from N2G import drawio_diagram
2 
3 diagram = drawio_diagram()#
4 diagram.add_diagram("Page-1")
5 diagram.add_node(id="R1")#增加節點
6 diagram.add_node(id="R2")#增加節點
7 diagram.add_link("R1", "R2", label="DF", src_label="Gi1/1", trgt_label="GE23")#增加節點之間的連線,標籤名稱,src_lable和trgt_lable 可以用來標註埠
8 diagram.layout(algo="kk")#圖層,不重要
9 diagram.dump_file(filename="Sample_graph.drawio", folder="./Output/")#儲存拓撲圖

以上示例是官方最簡單的畫圖程式,自己可以多增加幾個節點進行練習。

但是在網路情況裡,對每個鄰居節點都登入檢測檢查鄰居資訊時,會遇到同一個連線,在兩臺裝置上都能發現,那麼建立節點和連線時會出現什麼情況呢?

這裡官方有說明,如果發現節點已經存在就直接跳過(也可以自定義),這樣我們再寫程式上就簡單很多,連結裡有說明。

3.2 完成程式輸出

Cisco 交換機利用CDP資料自動繪製網路拓撲圖[drawio]-實踐
  1 from N2G import drawio_diagram
  2 import textfsm
  3 import json
  4 import telnetlib
  5 import time
  6 
  7 
  8 file_path = './host_1218.txt'
  9 #主機IP清單,格式如下:
 10 #cisco 192.168.1.1 cisco cisco
 11 
 12 
 13 template_file = "./cisco_tfm.template"
 14 style_cisco = "verticalLabelPosition=bottom;html=1;verticalAlign=top;aspect=fixed;align=center;pointerEvents=1;shape=mxgraph.cisco19.rect;prIcon=l2_switch;fillColor=#FAFAFA;strokeColor=#005073;"
 15 # 圖示格式
 16 
 17 def data_format(ip_address,cdp_data):
 18     
 19     hostname = cdp_data[0][0]#二層列表格式,獲取主機名
 20     result_data = {
 21         ip_address: {
 22             hostname: {item[1]: item[2:] for item in cdp_data}
 23         }
 24     }
 25     json_data = json.dumps(result_data, indent=2)
 26 
 27     return json_data
 28 # return the JSON formatted string
 29 
 30 
 31 def cisco_telent(ip, username, password, cmd):
 32     # 建立Telnet連線
 33     tn = telnetlib.Telnet(ip)
 34     time.sleep(0.1)
 35 
 36     tn.read_until(b"Username:")
 37     tn.write(username.encode('ascii') + b"\n")
 38 
 39     tn.read_until(b"Password: ")
 40     tn.write(password.encode('ascii') + b"\n")
 41     tn.write(b"terminal length 0\n")
 42 
 43     tn.write(cmd.encode('ascii') + b"\n")
 44 
 45     time.sleep(0.5)
 46     tn.write(b"exit\n")
 47     # # 讀取輸出並列印
 48     output = tn.read_very_eager().decode('ascii')
 49 
 50     tn.close() # 關閉連線
 51     return output
 52 
 53 
 54 diagram = drawio_diagram()
 55 diagram.add_diagram("Page-1")
 56 
 57 host_info_dict = {}
 58 
 59 node_num = 0
 60 #節點計數
 61 # 開啟檔案進行讀取
 62 with open(file_path, 'r') as file:
 63     for line in file:
 64         # 分割每一行以獲取主機資訊
 65         host_info = line.strip().split()
 66 
 67         # 檢查是否有足夠的資訊
 68         if len(host_info) == 4:
 69             device_type, ip, username, password = host_info
 70 
 71             # 構建裝置字典
 72             device = {
 73                 'device_type': device_type,
 74                 'ip': ip,
 75                 'username': username,
 76                 'password': password,
 77             }
 78 
 79             try:
 80                 # 執行telnet
 81 
 82                 cmd = 'show cdp neighbors detail'
 83 
 84                 command_output = cisco_telent(ip, username, password, cmd)
 85                 with open(template_file) as template:
 86                     #開啟模板,進行資料提取
 87                     fsm = textfsm.TextFSM(template)
 88                     result = fsm.ParseText(command_output)
 89                     #列表格式資料,需要進行轉換
 90 
 91                     nei_info_format = data_format(ip,result)
 92                     # print(nei_info_format)
 93                     parsed_data = json.loads(nei_info_format)
 94 
 95                     for ip_address, inner_data in parsed_data.items():
 96                         for local_hostname, cdp_data in inner_data.items():
 97                             if local_hostname not in host_info_dict:
 98                                 #節點不存在,進行建立節點並儲存節點資訊
 99                                 diagram.add_node(id=ip_address,name= local_hostname, style =style_cisco, width=60, height=60)
100                                 #增加本機節點
101                                 node_num += 1
102                                 # print(ip_address, local_hostname)
103                                 host_info_dict[local_hostname] = ip_address
104                                 # print(host_info_dict) 節點主機名和IP資訊儲存,後續進行校驗節點是否已經存在
105 
106                                 for src_label, values in cdp_data.items():
107                                     nei_host_name, nei_host_module, nei_host_id, trgt_label = values
108                                     #如果鄰居節點不存在,直接建立;如果節點已經存在,則鄰居ID資訊要更新,更換成已經存在節點的IP地址()
109                                     if nei_host_name not in host_info_dict:
110                                         diagram.add_node(id=nei_host_id,name=nei_host_name,style=style_cisco, width=60, height=60)
111                                         #根據鄰居資訊,增加鄰居節點
112                                         host_info_dict[nei_host_name] = nei_host_id
113                                         print("Add node " + nei_host_id + "secuessed.")
114                                         node_num += 1
115                                         diagram.add_link(ip_address, nei_host_id, src_label=src_label, trgt_label=trgt_label)
116                                     else:
117                                         nei_host_id = host_info_dict[nei_host_name]
118                                         diagram.add_link(ip_address, nei_host_id, src_label=src_label, trgt_label=trgt_label)
119                             else:
120                                 print('節點已存在:' + local_hostname + ip_address)
121                                 #節點存在,檢查鄰居資訊,根據鄰居資訊進行判斷,是否新增鄰居節點
122                                 for src_label, values in cdp_data.items():
123                                     nei_host_name, nei_host_module, nei_host_id, trgt_label = values
124                                     if nei_host_name not in host_info_dict:
125                                         diagram.add_node(id=nei_host_id, name=nei_host_name, style=style_cisco, width=60, height=60)
126                                         #根據鄰居資訊,增加鄰居節點
127                                         host_info_dict[nei_host_name] = nei_host_id
128                                         print("Add node " + nei_host_id + "secuessed.")
129                                         node_num += 1
130                                         diagram.add_link(ip_address, nei_host_id, src_label=src_label, trgt_label=trgt_label)
131                                     else:
132                                         nei_host_id = host_info_dict[nei_host_name]
133                                         diagram.add_link(ip_address, nei_host_id, src_label=src_label, trgt_label=trgt_label)
134 
135                             # print("Add node " + ip_address + " sucessed.")        
136 
137             except Exception as e:
138                 print(f"Failed to connect to {ip}: {e}")
139 
140         else:
141             print(f"Invalid line in the file: {line.strip()}")
142 print(node_num)
143 print(host_info_dict,len(host_info_dict))
144 diagram.dump_file(filename="Sample_campus_00.drawio", folder="./Output/")
draw_topology_N2G

 

以上指令碼,除路徑本人修改過以外,整個程式在網路環境中進行的驗證。

說明:

1、在第57行,建立了1個字典,用來儲存發現的鄰居資料,主機名、主機名稱,用來後續對新發現節點進行判斷。為什麼還需要進行判斷?

    因為當一臺裝置上有多個IP地址時,與之互聯的裝置鄰居資訊顯示的IP資訊可能是不一樣的,所以再次透過主機名再次進行判斷;當然這裡也不是很嚴謹,因為裝置名稱可能存在全域性不唯一的情況,所以在cisco NX-OS中,CDP鄰居資訊會顯示裝置的序列號,這樣就避免前面的情況。

2、因為網路規模較大,輸出的拓撲檔案也比較大,所以在第59行,增加了一個計數,在每增加1個節點,都進行一個儲存資訊,最終輸出看有多少網路裝置。

3、為什麼add_node 方法中有非官方說明的欄位“name”.

    答:非標準欄位,生成的拓撲圖會以屬性欄位描述節點,滑鼠放到節點上就可顯示,不會直接顯示出來,包括前面已經提取到的裝置型號,也可以透過增加欄位module在拓撲圖中隱性的展示;右鍵成編輯節點資料,即可檢視或編輯資料

 四、因為在程式中沒有指定每個節點的座標位置,所以生成的拓撲圖,所有的圖示節點都是在預設位置,所以會出現重疊的情況,只需要在調整圖形-佈局,選擇“垂直流“就可自動重新排列。

 

相關文章