哈Ha!本文是根据最近的Cisco DevNet在线马拉松中的任务解决方案编写的。要求参与者自动分析和可视化任意网络拓扑以及其中发生的变化。
这项任务并不是最简单的,在Blog领域中,有关该主题的文章很少。下面,我对我们自己的实现进行了讨论,并对使用的工具和方法进行了描述。
感兴趣的大家欢迎来猫!

*而且不是很多Javascript。
免责声明
, .
, .
MIT .
.
, , Ctrl+Enter ⌘+Enter .
:
, L2/L3 IOS/IOSXE. IP- , IP, show-. , , SNMP. .
: LLDP (, ). (, ).
c :
• ( ).
• Hostname .
• ( , , GigabitEthernet0/0 — G0/0).
, () .
: ( ) .
— IP- , — . - .
:
- vs .
. . - .
, . , - . - .
, . - .
. - .
.
, . . , , , .
:

:
- .
IOS IOS-XE.
. .
- .
LLDP (Link Layer Discovery Protocol), , , (L2) OSI. , IEEE 802.1AB. Linux Windows, .
, , .. .
- .
NETCONF, REST API, RESTCONF YANG . SSH, Telnet CLI. - - - /.
, Python, .. , .
API requests .
SSH/Telnet netmiko, scrapli, paramiko. CLI Python — .. , , .
, . NAPALM Nornir. NAPALM - GETTER' , LLDP. Nornir .
SNMP . - -> -> .
API , , CLI . re . TextFSM Google .
NAPALM GETTER' , . - .
, , . - .
, .
, . Python (pygraphviz, matplotlib, networkx) JS D3.js, vis.js. JS+HTML5 Toolkit NeXt UI, Cisco DevNet . , .
. HTML-, - .
, :

:
NAPALM Nornir . NAPALM LLDP Cisco IOS/IOSXE, IOS-XR, NX-OS, Juniper JunOS Arista EOS.
, , .
Next UI , .
Cisco Modeling Labs. VIRL. Cisco DevNet Sandbox , ( ) VPN- ( AnyConnect). - GNS3. :)
CML, - :

IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02), NXOS (dist-sw01, dist-sw02), IOSXR (core-rtr01, core-rtr02) ASA (edge-firewall01). LLDP. SSH IOS, IOSXE NXOS .
Nornir
Nornir Python-. PyPI, Python 3.6.2 . , NAPALM netmiko. Python (venv) . MacOS, Linux- Windows .
$ mkdir ~/testenv
$ python3.7 -m venv ~/testenv/
$ source ~/testenv/bin/activate
(testenv)$ pip install nornir
Nornir inventory .
SimpleInventory.
Nornir yaml , , Python-.
nornir_config.yaml:
---
core:
num_workers: 20
inventory:
plugin: nornir.plugins.inventory.simple.SimpleInventory
options:
host_file: "inventory/hosts_devnet_sb_cml.yml"
group_file: "inventory/groups.yml"
, yaml-: . . — . , . , .
num_workers Nornir , . 20.
inventory/hosts_devnet_sb_cml.yml :
---
internet-rtr01:
hostname: 10.10.20.181
platform: ios
groups:
- devnet-cml-lab
dist-sw01:
hostname: 10.10.20.177
platform: nxos_ssh
transport: ssh
groups:
- devnet-cml-lab
. IP- , ( IOS SSH) . 'devnet-cml-lab'.
groups.yml :
---
devnet-cml-lab:
username: cisco
password: cisco
connection_options:
napalm:
extras:
optional_args:
secret: cisco
, enable Cisco. .
! , .
, Nornir Python- .
NeXt UI
GitHub, . ./next_sources.
:
$ tree . -L 2
.
├── inventory
│ ├── groups.yml
│ └── hosts_devnet_sb_cml.yml
├── next_sources
│ ├── css
│ ├── doc
│ ├── fonts
│ └── js
├── nornir_config.yml
80
generate_topology.py.
Nornir
Nornir Python:
from nornir import InitNornir
from nornir.plugins.tasks.networking import napalm_get
NORNIR_CONFIG_FILE = "nornir_config.yml"
nr = InitNornir(config_file=NORNIR_CONFIG_FILE)
.
napalm_get NAPALM Nornir.
LLDP
LLDP , TLV . LLDP- .
TLV: Chassis ID, Port ID Time-to-Live
: System name and description; Port name and description; VLAN name; IP management address; System capabilities (switching, routing, etc.) .
.. , System name Port name TLV.
, - control plane (, ) .
(.. ).
, , OSPF LSA. — . LLDP.
edge, core distribution . internet-rtr01 LLDP-.
, dist-rtr01:
dist-rtr01#sh lldp nei
Capability codes:
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
dist-rtr02.devnet.laGi6 120 R Gi6
dist-sw01.devnet.labGi4 120 B,R Ethernet1/3
dist-sw02.devnet.labGi5 120 B,R Ethernet1/3
core-rtr02.devnet.laGi3 120 R Gi0/0/0/2
core-rtr01.devnet.laGi2 120 R Gi0/0/0/2
Total entries displayed: 5
, .
core-rtr02:
RP/0/0/CPU0:core-rtr02#sh lldp nei
Sun May 10 22:07:05.776 UTC
Capability codes:
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
core-rtr01.devnet.la Gi0/0/0/0 120 R Gi0/0/0/0
edge-sw01.devnet.lab Gi0/0/0/1 120 R Gi0/3
dist-rtr01.devnet.la Gi0/0/0/2 120 R Gi3
dist-rtr02.devnet.la Gi0/0/0/3 120 R Gi3
Total entries displayed: 4
4 , .
, Device ID.
— CLI-.
.
:
'show lldp neighbors detail' dist-rtr01 IOSXEdist-rtr01#sh lldp nei det
------------------------------------------------
Local Intf: Gi6
Chassis id: 001e.e57c.cf00
Port id: Gi6
Port Description: L3 Link to dist-rtr01
System Name: dist-rtr02.devnet.lab
System Description:
Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45
Time remaining: 91 seconds
System Capabilities: B,R
Enabled Capabilities: R
Management Addresses:
IP: 172.16.252.18
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised
------------------------------------------------
Local Intf: Gi4
Chassis id: 5254.0007.5d59
Port id: Ethernet1/3
Port Description: L3 link to dist-rtr01
System Name: dist-sw01.devnet.lab
System Description:
Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 108 seconds
System Capabilities: B,R
Enabled Capabilities: B,R
Management Addresses:
IP: 10.10.20.177
Other: 52 54 00 07 5D 59 00
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised
------------------------------------------------
Local Intf: Gi5
Chassis id: 5254.0007.b7e6
Port id: Ethernet1/3
Port Description: L3 link to dist-rtr01
System Name: dist-sw02.devnet.lab
System Description:
Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 97 seconds
System Capabilities: B,R
Enabled Capabilities: B,R
Management Addresses:
IP: 10.10.20.178
Other: 52 54 00 07 FF FF 00
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised
------------------------------------------------
Local Intf: Gi3
Chassis id: 02c7.9dc0.0c06
Port id: Gi0/0/0/2
Port Description: L3 Link to dist-rtr01
System Name: core-rtr02.devnet.lab
System Description:
Cisco IOS XR Software, Version 6.3.1[Default]
Copyright (c) 2017 by Cisco Systems, Inc., IOS XRv Series
Time remaining: 94 seconds
System Capabilities: R
Enabled Capabilities: R
Management Addresses:
IP: 172.16.252.26
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised
------------------------------------------------
Local Intf: Gi2
Chassis id: 0288.15c0.0c06
Port id: Gi0/0/0/2
Port Description: L3 Link to dist-rtr01
System Name: core-rtr01.devnet.lab
System Description:
Cisco IOS XR Software, Version 6.3.1[Default]
Copyright (c) 2017 by Cisco Systems, Inc., IOS XRv Series
Time remaining: 110 seconds
System Capabilities: R
Enabled Capabilities: R
Management Addresses:
IP: 172.16.252.22
Auto Negotiation - not supported
Physical media capabilities - not advertised
Media Attachment Unit type - not advertised
Vlan ID: - not advertised
Total entries displayed: 5
show lldp neighbors detail c dist-sw01 NXOSdist-sw01# sh lldp nei det
Capability codes:
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
Chassis id: 5254.0007.b7e4
Port id: Ethernet1/1
Local Port id: Eth1/1
Port Description: VPC Peer Link
System Name: dist-sw02.devnet.lab
System Description: Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 112 seconds
System Capabilities: B, R
Enabled Capabilities: B, R
Management Address: 10.10.20.178
Management Address IPV6: not advertised
Vlan ID: 1
Chassis id: 5254.0007.b7e5
Port id: Ethernet1/2
Local Port id: Eth1/2
Port Description: VPC Peer Link
System Name: dist-sw02.devnet.lab
System Description: Cisco Nexus Operating System (NX-OS) Software 9.2(3)
TAC support: http://www.cisco.com/tac
Copyright (c) 2002-2019, Cisco Systems, Inc. All rights reserved.
Time remaining: 112 seconds
System Capabilities: B, R
Enabled Capabilities: B, R
Management Address: 10.10.20.178
Management Address IPV6: not advertised
Vlan ID: 1
Chassis id: 001e.7a2a.3900
Port id: Gi4
Local Port id: Eth1/3
Port Description: L3 Link to dist-sw01
System Name: dist-rtr01.devnet.lab
System Description: Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45
Time remaining: 109 seconds
System Capabilities: B, R
Enabled Capabilities: R
Management Address: 172.16.252.2
Management Address IPV6: not advertised
Vlan ID: not advertised
Chassis id: 001e.e57c.cf00
Port id: Gi4
Local Port id: Eth1/4
Port Description: L3 Link to dist-sw01
System Name: dist-rtr02.devnet.lab
System Description: Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Tue 28-May-19 12:45
Time remaining: 108 seconds
System Capabilities: B, R
Enabled Capabilities: R
Management Address: 172.16.252.6
Management Address IPV6: not advertised
Vlan ID: not advertised
Total entries displayed: 4
IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02) NXOS (dist-sw01, dist-sw02).
IOSXR (core-rtr01, core-rtr02) .
:
- distribution .
. - core-rtr01 core-rtr02.
. - .
edge-sw01, distr-rtr01 distr-sw02 core-rtr01 core-rtr02 LLDP.
.
inventory/hosts_devnet_sb_cml.yml---
internet-rtr01:
hostname: 10.10.20.181
platform: ios
site: devnet_sandbox
groups:
- devnet-cml-lab
edge-sw01:
hostname: 10.10.20.172
platform: ios
site: devnet_sandbox
groups:
- devnet-cml-lab
core-rtr01:
#
hostname: 10.10.20.173
platform: iosxr
groups:
- devnet-cml-lab
core-rtr02:
#
hostname: 10.10.20.174
platform: iosxr
groups:
- devnet-cml-lab
dist-rtr01:
hostname: 10.10.20.175
platform: ios
groups:
- devnet-cml-lab
dist-rtr02:
hostname: 10.10.20.176
platform: ios
groups:
- devnet-cml-lab
dist-sw01:
hostname: 10.10.20.177
platform: nxos_ssh
transport: ssh
groups:
- devnet-cml-lab
dist-sw02:
hostname: 10.10.20.178
platform: nxos_ssh
transport: ssh
groups:
- devnet-cml-lab
NAPALM:
- GET_LLDP_NEIGHBORS_DETAILS ( LLDP-).
, .. CLI- . - GET_FACTS ( ).
FQDN, .
, . .
-Task Nornir.
.
num_workers .
def get_host_data(task):
"""Nornir Task ."""
task.run(
task=napalm_get,
getters=['facts', 'lldp_neighbors_detail']
)
get_host_data_result = nr.run(get_host_data)
, Nornir .
get_host_data_result get_host_data .
>>> get_host_data_result
AggregatedResult (get_host_data): {'internet-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'edge-sw01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'core-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'core-rtr02': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-rtr02': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-sw01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'dist-sw02': MultiResult: [Result: "get_host_data", Result: "napalm_get"]}
failed, , , .
:
>>> for device, result in get_host_data_result.items():
... print(f'{device} failed: {result.failed}')
...
internet-rtr01 failed: False
edge-sw01 failed: False
core-rtr01 failed: True
core-rtr02 failed: True
dist-rtr01 failed: False
dist-rtr02 failed: False
dist-sw01 failed: False
dist-sw02 failed: False
.
:
dist-rtr01>>> get_host_data_result['dist-rtr01'][1].result
{'facts': {'uptime': 6120, 'vendor': 'Cisco', 'os_version': 'Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'serial_number': '9JDCOVUDSWN', 'model': 'CSR1000V', 'hostname': 'dist-rtr01', 'fqdn': 'dist-rtr01.devnet.lab', 'interface_list': ['GigabitEthernet1', 'GigabitEthernet2', 'GigabitEthernet3', 'GigabitEthernet4', 'GigabitEthernet5', 'GigabitEthernet6', 'Loopback0']}, 'lldp_neighbors_detail': {'GigabitEthernet6': [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi6', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'GigabitEthernet4': [{'remote_chassis_id': '5254.0007.5d59', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw01.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'GigabitEthernet5': [{'remote_chassis_id': '5254.0007.b7e6', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'GigabitEthernet3': [{'remote_chassis_id': '02c7.9dc0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'GigabitEthernet2': [{'remote_chassis_id': '0288.15c0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}}
dist-sw01>>> get_host_data_result['dist-sw01'][1].result
{'facts': {'uptime': 6090, 'vendor': 'Cisco', 'os_version': '9.2(3)', 'serial_number': '9P5OMCCMSQ4', 'model': 'Nexus9000 9000v Chassis', 'hostname': 'dist-sw01', 'fqdn': 'dist-sw01.devnet.lab', 'interface_list': ['mgmt0', 'Ethernet1/1', 'Ethernet1/2', 'Ethernet1/3', 'Ethernet1/4', 'Ethernet1/5', 'Ethernet1/6', 'Ethernet1/7', 'Ethernet1/8', 'Ethernet1/9', 'Ethernet1/10', 'Ethernet1/11', 'Ethernet1/12', 'Ethernet1/13', 'Ethernet1/14', 'Ethernet1/15', 'Ethernet1/16', 'Ethernet1/17', 'Ethernet1/18', 'Ethernet1/19', 'Ethernet1/20', 'Ethernet1/21', 'Ethernet1/22', 'Ethernet1/23', 'Ethernet1/24', 'Ethernet1/25', 'Ethernet1/26', 'Ethernet1/27', 'Ethernet1/28', 'Ethernet1/29', 'Ethernet1/30', 'Ethernet1/31', 'Ethernet1/32', 'Ethernet1/33', 'Ethernet1/34', 'Ethernet1/35', 'Ethernet1/36', 'Ethernet1/37', 'Ethernet1/38', 'Ethernet1/39', 'Ethernet1/40', 'Ethernet1/41', 'Ethernet1/42', 'Ethernet1/43', 'Ethernet1/44', 'Ethernet1/45', 'Ethernet1/46', 'Ethernet1/47', 'Ethernet1/48', 'Ethernet1/49', 'Ethernet1/50', 'Ethernet1/51', 'Ethernet1/52', 'Ethernet1/53', 'Ethernet1/54', 'Ethernet1/55', 'Ethernet1/56', 'Ethernet1/57', 'Ethernet1/58', 'Ethernet1/59', 'Ethernet1/60', 'Ethernet1/61', 'Ethernet1/62', 'Ethernet1/63', 'Ethernet1/64', 'Ethernet1/65', 'Ethernet1/66', 'Ethernet1/67', 'Ethernet1/68', 'Ethernet1/69', 'Ethernet1/70', 'Ethernet1/71', 'Ethernet1/72', 'Ethernet1/73', 'Ethernet1/74', 'Ethernet1/75', 'Ethernet1/76', 'Ethernet1/77', 'Ethernet1/78', 'Ethernet1/79', 'Ethernet1/80', 'Ethernet1/81', 'Ethernet1/82', 'Ethernet1/83', 'Ethernet1/84', 'Ethernet1/85', 'Ethernet1/86', 'Ethernet1/87', 'Ethernet1/88', 'Ethernet1/89', 'Ethernet1/90', 'Ethernet1/91', 'Ethernet1/92', 'Ethernet1/93', 'Ethernet1/94', 'Ethernet1/95', 'Ethernet1/96', 'Ethernet1/97', 'Ethernet1/98', 'Ethernet1/99', 'Ethernet1/100', 'Ethernet1/101', 'Ethernet1/102', 'Ethernet1/103', 'Ethernet1/104', 'Ethernet1/105', 'Ethernet1/106', 'Ethernet1/107', 'Ethernet1/108', 'Ethernet1/109', 'Ethernet1/110', 'Ethernet1/111', 'Ethernet1/112', 'Ethernet1/113', 'Ethernet1/114', 'Ethernet1/115', 'Ethernet1/116', 'Ethernet1/117', 'Ethernet1/118', 'Ethernet1/119', 'Ethernet1/120', 'Ethernet1/121', 'Ethernet1/122', 'Ethernet1/123', 'Ethernet1/124', 'Ethernet1/125', 'Ethernet1/126', 'Ethernet1/127', 'Ethernet1/128', 'Port-channel1', 'Loopback0', 'Vlan1', 'Vlan101', 'Vlan102', 'Vlan103', 'Vlan104', 'Vlan105']}, 'lldp_neighbors_detail': {'Ethernet1/1': [{'remote_chassis_id': '5254.0007.b7e4', 'remote_port': 'Ethernet1/1', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'Ethernet1/2': [{'remote_chassis_id': '5254.0007.b7e5', 'remote_port': 'Ethernet1/2', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}], 'Ethernet1/3': [{'remote_chassis_id': '001e.7a2a.3900', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}], 'Ethernet1/4': [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}}
'facts' 'lldp_neighbors_detail' .
NAPALM' .
:
dist-rtr01>>> for neighbor in get_host_data_result['dist-rtr01'][1].result['lldp_neighbors_detail'].items():
... print(neighbor)
... print('\n')
...
('GigabitEthernet6', [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi6', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])
('GigabitEthernet4', [{'remote_chassis_id': '5254.0007.5d59', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw01.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])
('GigabitEthernet5', [{'remote_chassis_id': '5254.0007.b7e6', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 link to dist-rtr01', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])
('GigabitEthernet3', [{'remote_chassis_id': '02c7.9dc0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])
('GigabitEthernet2', [{'remote_chassis_id': '0288.15c0.0c06', 'remote_port': 'Gi0/0/0/2', 'remote_port_description': 'L3 Link to dist-rtr01', 'remote_system_name': 'core-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])
dist-sw01>>> for neighbor in get_host_data_result['dist-sw01'][1].result['lldp_neighbors_detail'].items():
... print(neighbor)
... print('\n')
...
('Ethernet1/1', [{'remote_chassis_id': '5254.0007.b7e4', 'remote_port': 'Ethernet1/1', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])
('Ethernet1/2', [{'remote_chassis_id': '5254.0007.b7e5', 'remote_port': 'Ethernet1/2', 'remote_port_description': 'VPC Peer Link', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge', 'router'], 'parent_interface': ''}])
('Ethernet1/3', [{'remote_chassis_id': '001e.7a2a.3900', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr01.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])
('Ethernet1/4', [{'remote_chassis_id': '001e.e57c.cf00', 'remote_port': 'Gi4', 'remote_port_description': 'L3 Link to dist-sw01', 'remote_system_name': 'dist-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}])
5 dist-rtr01, CLI .
4 dist-sw01, .
.
LLDP .
:
- FQDN, ( ).
- hostname, .
- - inventory Nornir.
LLDP.
def normalize_result(nornir_job_result):
"""
get_host_data.
LLDP FACTS
.
"""
global_lldp_data = {}
global_facts = {}
for device, output in nornir_job_result.items():
if output[0].failed:
global_lldp_data[device] = {}
global_facts[device] = {
'nr_ip': nr.inventory.hosts[device].get('hostname', 'n/a'),
}
continue
device_fqdn = output[1].result['facts']['fqdn']
if not device_fqdn:
device_fqdn = output[1].result['facts']['hostname']
if not device_fqdn:
device_fqdn = device
global_facts[device_fqdn] = output[1].result['facts']
global_facts[device_fqdn]['nr_ip'] = nr.inventory.hosts[device].get('hostname', 'n/a')
global_lldp_data[device_fqdn] = output[1].result['lldp_neighbors_detail']
return global_lldp_data, global_facts
LLDP :
:
((source_device_id, source_port_name), (destination_device_id, destination_port_name))
, :
- , .
. - LLDP . , GigabitEthernet4 , Gi4 .
. .
capabilities, LLDP. .
:
interface_full_name_map = {
'Eth': 'Ethernet',
'Fa': 'FastEthernet',
'Gi': 'GigabitEthernet',
'Te': 'TenGigabitEthernet',
}
def if_fullname(ifname):
for k, v in interface_full_name_map.items():
if ifname.startswith(v):
return ifname
if ifname.startswith(k):
return ifname.replace(k, v)
return ifname
def if_shortname(ifname):
for k, v in interface_full_name_map.items():
if ifname.startswith(v):
return ifname.replace(v, k)
return ifname
def extract_lldp_details(lldp_data_dict):
"""
LLDP-.
,
LLDP capabilities
.
"""
discovered_hosts = set()
lldp_capabilities_dict = {}
global_interconnections = []
for host, lldp_data in lldp_data_dict.items():
if not host:
continue
discovered_hosts.add(host)
if not lldp_data:
continue
for interface, neighbors in lldp_data.items():
for neighbor in neighbors:
if not neighbor['remote_system_name']:
continue
discovered_hosts.add(neighbor['remote_system_name'])
if neighbor['remote_system_enable_capab']:
lldp_capabilities_dict[neighbor['remote_system_name']] = (
neighbor['remote_system_enable_capab'][0]
)
else:
lldp_capabilities_dict[neighbor['remote_system_name']] = ''
local_end = (host, interface)
remote_end = (
neighbor['remote_system_name'],
if_fullname(neighbor['remote_port'])
)
link_is_already_there = (
(local_end, remote_end) in global_interconnections
or (remote_end, local_end) in global_interconnections
)
if link_is_already_there:
continue
global_interconnections.append((
(host, interface),
(neighbor['remote_system_name'], if_fullname(neighbor['remote_port']))
))
return [discovered_hosts, global_interconnections, lldp_capabilities_dict]
NeXt UI
next_app.js NeXt UI.
:
(function (nx) {
var topo = new nx.graphic.Topology({
width: 1200,
height: 700,
dataProcessor: 'force',
identityKey: 'id',
nodeConfig: {
label: 'model.name',
iconType:'model.icon',
},
linkConfig: {
linkType: 'curve',
},
showIcon: true,
});
var Shell = nx.define(nx.ui.Application, {
methods: {
start: function () {
topo.data(topologyData);
topo.attach(this);
}
}
});
var shell = new Shell();
shell.start();
})(nx);
topologyData, topology.js. .
HTML , :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="next_sources/css/next.css">
<link rel="stylesheet" href="styles_main_page.css">
<script src="next_sources/js/next.js"></script>
<script src="topology.js"></script>
<script src="next_app.js"></script>
</head>
<body>
</body>
</html>
Python.
:
GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result)
TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA)
NeXt UI :
var topologyData = {
"links": [
{
"id": 0,
"source": 0,
"target": 1,
}, {
"id": 1,
"source": 0,
"target": 1,
}
],
"nodes": [
{
"icon": "router",
"id": 0,
},
{
"icon": "router",
"id": 1,
}
]
, JSON , :
{'nodes': [], 'links': []} Python.
.
, capabilities LLDP , .
FACTS (, ), .
icon_capability_map = {
'router': 'router',
'switch': 'switch',
'bridge': 'switch',
'station': 'host'
}
icon_model_map = {
'CSR1000V': 'router',
'Nexus': 'switch',
'IOSXRv': 'router',
'IOSv': 'switch',
'2901': 'router',
'2911': 'router',
'2921': 'router',
'2951': 'router',
'4321': 'router',
'4331': 'router',
'4351': 'router',
'4421': 'router',
'4431': 'router',
'4451': 'router',
'2960': 'switch',
'3750': 'switch',
'3850': 'switch',
}
def get_icon_type(device_cap_name, device_model=''):
"""
.
LLDP capabilities.
,
.
'unknown'.
"""
if device_cap_name:
icon_type = icon_capability_map.get(device_cap_name)
if icon_type:
return icon_type
if device_model:
for model_shortname, icon_type in icon_model_map.items():
if model_shortname in device_model:
return icon_type
return 'unknown'
def generate_topology_json(*args):
"""
JSON- .
,
LLDP capabilities
,
.
"""
discovered_hosts, interconnections, lldp_capabilities_dict, facts = args
host_id = 0
host_id_map = {}
topology_dict = {'nodes': [], 'links': []}
for host in discovered_hosts:
device_model = 'n/a'
device_serial = 'n/a'
device_ip = 'n/a'
if facts.get(host):
device_model = facts[host].get('model', 'n/a')
device_serial = facts[host].get('serial_number', 'n/a')
device_ip = facts[host].get('nr_ip', 'n/a')
host_id_map[host] = host_id
topology_dict['nodes'].append({
'id': host_id,
'name': host,
'primaryIP': device_ip,
'model': device_model,
'serial_number': device_serial,
'icon': get_icon_type(
lldp_capabilities_dict.get(host, ''),
device_model
)
})
host_id += 1
link_id = 0
for link in interconnections:
topology_dict['links'].append({
'id': link_id,
'source': host_id_map[link[0][0]],
'target': host_id_map[link[1][0]],
'srcIfName': if_shortname(link[0][1]),
'srcDevice': link[0][0],
'tgtIfName': if_shortname(link[1][1]),
'tgtDevice': link[1][0],
})
link_id += 1
return topology_dict
, topology.js, json :
import json
OUTPUT_TOPOLOGY_FILENAME = 'topology.js'
TOPOLOGY_FILE_HEAD = "\n\nvar topologyData = "
def write_topology_file(topology_json, header=TOPOLOGY_FILE_HEAD, dst=OUTPUT_TOPOLOGY_FILENAME):
with open(dst, 'w') as topology_file:
topology_file.write(header)
topology_file.write(json.dumps(topology_json, indent=4, sort_keys=True))
topology_file.write(';')
TOPOLOGY_DICT = generate_topology_json(*TOPOLOGY_DETAILS)
write_topology_file(TOPOLOGY_DICT)
topology.js
var topologyData = {
"links": [
{
"id": 0,
"source": 7,
"srcDevice": "edge-sw01.devnet.lab",
"srcIfName": "Gi0/2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/1"
},
{
"id": 1,
"source": 7,
"srcDevice": "edge-sw01.devnet.lab",
"srcIfName": "Gi0/3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/1"
},
{
"id": 2,
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/2"
},
{
"id": 3,
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi4",
"target": 1,
"tgtDevice": "dist-sw01.devnet.lab",
"tgtIfName": "Eth1/3"
},
{
"id": 4,
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi6",
"target": 0,
"tgtDevice": "dist-rtr02.devnet.lab",
"tgtIfName": "Gi6"
},
{
"id": 5,
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi5",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/3"
},
{
"id": 6,
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/2"
},
{
"id": 7,
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/3"
},
{
"id": 8,
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi4",
"target": 1,
"tgtDevice": "dist-sw01.devnet.lab",
"tgtIfName": "Eth1/4"
},
{
"id": 9,
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi5",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/4"
},
{
"id": 10,
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/3"
},
{
"id": 11,
"source": 1,
"srcDevice": "dist-sw01.devnet.lab",
"srcIfName": "Eth1/1",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/1"
},
{
"id": 12,
"source": 1,
"srcDevice": "dist-sw01.devnet.lab",
"srcIfName": "Eth1/2",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/2"
}
],
"nodes": [
{
"icon": "router",
"id": 0,
"model": "CSR1000V",
"name": "dist-rtr02.devnet.lab",
"serial_number": "9YZKNQKQ566",
"layerSortPreference": 7,
"primaryIP": "10.10.20.176",
"dcimDeviceLink": "http://localhost:32768/dcim/devices/?q=dist-rtr02.devnet.lab"
},
{
"icon": "switch",
"id": 1,
"model": "Nexus9000 9000v Chassis",
"name": "dist-sw01.devnet.lab",
"serial_number": "9MZLNM0ZC9Z",
},
{
"icon": "switch",
"id": 2,
"model": "Nexus9000 9000v Chassis",
"name": "dist-sw02.devnet.lab",
"serial_number": "93LCGCRUJA5",
},
{
"icon": "router",
"id": 3,
"model": "n/a",
"name": "core-rtr02.devnet.lab",
"serial_number": "n/a",
},
{
"icon": "router",
"id": 4,
"model": "CSR1000V",
"name": "dist-rtr01.devnet.lab",
"serial_number": "9S78ZRF2V2B",
},
{
"icon": "router",
"id": 5,
"model": "n/a",
"name": "core-rtr01.devnet.lab",
"serial_number": "n/a",
},
{
"icon": "router",
"id": 6,
"model": "CSR1000V",
"name": "internet-rtr01.virl.info",
"serial_number": "9LGWPM8GTV6",
},
{
"icon": "switch",
"id": 7,
"model": "IOSv",
"name": "edge-sw01.devnet.lab",
"serial_number": "927A4RELIGI",
}
]
};
main.html Hello World:

. .
, NeXt UI , :

, . , .
.
. , :
- cached_topology.json .
generate_topology.py . - diff_topology.js .
- diff_page.html .
HTML- :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="next_sources/css/next.css">
<link rel="stylesheet" href="styles_main_page.css">
<script src="next_sources/js/next.js"></script>
<script src="diff_topology.js"></script>
<script src="next_app.js"></script>
</head>
<body>
<a href="main.html"><button> </button></a>
</p>
</body>
</html>
:
CACHED_TOPOLOGY_FILENAME = 'cached_topology.json'
def write_topology_cache(topology_json, dst=CACHED_TOPOLOGY_FILENAME):
with open(dst, 'w') as cached_file:
cached_file.write(json.dumps(topology_json, indent=4, sort_keys=True))
def read_cached_topology(filename=CACHED_TOPOLOGY_FILENAME):
if not os.path.exists(filename):
return {}
if not os.path.isfile(filename):
return {}
cached_topology = {}
with open(filename, 'r') as file:
try:
cached_topology = json.loads(file.read())
except:
return {}
return cached_topology
:
- .
:
( , (,))
:
( , (_, ), ( , _))
. - ( -).
:
- diff_nodes = {'added': [], 'deleted': []}
- diff_links = {'added': [], 'deleted': []}
- .
diff_merged_topology.
is_new is_dead.
'dead_node' ( NeXt UI ).
:
def get_topology_diff(cached, current):
"""
.
.
,
.
"""
diff_nodes = {'added': [], 'deleted': []}
diff_links = {'added': [], 'deleted': []}
diff_merged_topology = {'nodes': [], 'links': []}
cached_links = [(x, ((x['srcDevice'], x['srcIfName']), (x['tgtDevice'], x['tgtIfName']))) for x in cached['links']]
links = [(x, ((x['srcDevice'], x['srcIfName']), (x['tgtDevice'], x['tgtIfName']))) for x in current['links']]
cached_nodes = [(x, (x['name'],)) for x in cached['nodes']]
nodes = [(x, (x['name'],)) for x in current['nodes']]
node_id = 0
host_id_map = {}
for raw_data, node in nodes:
if node in [x[1] for x in cached_nodes]:
raw_data['id'] = node_id
host_id_map[raw_data['name']] = node_id
raw_data['is_new'] = 'no'
raw_data['is_dead'] = 'no'
diff_merged_topology['nodes'].append(raw_data)
node_id += 1
continue
diff_nodes['added'].append(node)
raw_data['id'] = node_id
host_id_map[raw_data['name']] = node_id
raw_data['is_new'] = 'yes'
raw_data['is_dead'] = 'no'
diff_merged_topology['nodes'].append(raw_data)
node_id += 1
for raw_data, cached_node in cached_nodes:
if cached_node in [x[1] for x in nodes]:
continue
diff_nodes['deleted'].append(cached_node)
raw_data['id'] = node_id
host_id_map[raw_data['name']] = node_id
raw_data['is_new'] = 'no'
raw_data['is_dead'] = 'yes'
raw_data['icon'] = 'dead_node'
diff_merged_topology['nodes'].append(raw_data)
node_id += 1
link_id = 0
for raw_data, link in links:
src, dst = link
if not (src, dst) in [x[1] for x in cached_links] and not (dst, src) in [x[1] for x in cached_links]:
diff_links['added'].append((src, dst))
raw_data['id'] = link_id
link_id += 1
raw_data['source'] = host_id_map[src[0]]
raw_data['target'] = host_id_map[dst[0]]
raw_data['is_new'] = 'yes'
raw_data['is_dead'] = 'no'
diff_merged_topology['links'].append(raw_data)
continue
raw_data['id'] = link_id
link_id += 1
raw_data['source'] = host_id_map[src[0]]
raw_data['target'] = host_id_map[dst[0]]
raw_data['is_new'] = 'no'
raw_data['is_dead'] = 'no'
diff_merged_topology['links'].append(raw_data)
for raw_data, link in cached_links:
src, dst = link
if not (src, dst) in [x[1] for x in links] and not (dst, src) in [x[1] for x in links]:
diff_links['deleted'].append((src, dst))
raw_data['id'] = link_id
link_id += 1
raw_data['source'] = host_id_map[src[0]]
raw_data['target'] = host_id_map[dst[0]]
raw_data['is_new'] = 'no'
raw_data['is_dead'] = 'yes'
diff_merged_topology['links'].append(raw_data)
return diff_nodes, diff_links, diff_merged_topology
get_topology_diff .
.
:
def print_diff(diff_result):
"""
get_topology_diff .
"""
diff_nodes, diff_links, *ignore = diff_result
if not (diff_nodes['added'] or diff_nodes['deleted'] or diff_links['added'] or diff_links['deleted']):
print(' .')
return
print(' :')
if diff_nodes['added']:
print('')
print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
print(' :')
print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
for node in diff_nodes['added']:
print(f' : {node[0]}')
if diff_nodes['deleted']:
print('')
print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
print(' :')
print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
for node in diff_nodes['deleted']:
print(f' : {node[0]}')
if diff_links['added']:
print('')
print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
print(' :')
print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
for src, dst in diff_links['added']:
print(f' {src[0]}({src[1]}) {dst[0]}({dst[1]})')
if diff_links['deleted']:
print('')
print('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
print(' :')
print('vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
for src, dst in diff_links['deleted']:
print(f' {src[0]}({src[1]}) {dst[0]}({dst[1]})')
print('')
main() :
def good_luck_have_fun():
""", ."""
get_host_data_result = nr.run(get_host_data)
GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result)
TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA)
TOPOLOGY_DETAILS.append(GLOBAL_FACTS)
TOPOLOGY_DICT = generate_topology_json(*TOPOLOGY_DETAILS)
CACHED_TOPOLOGY = read_cached_topology()
write_topology_file(TOPOLOGY_DICT)
write_topology_cache(TOPOLOGY_DICT)
print(f' main.html')
if CACHED_TOPOLOGY:
DIFF_DATA = get_topology_diff(CACHED_TOPOLOGY, TOPOLOGY_DICT)
print_diff(DIFF_DATA)
write_topology_file(DIFF_DATA[2], dst='diff_topology.js')
else:
write_topology_file(TOPOLOGY_DICT, dst='diff_topology.js')
if __name__ == '__main__':
good_luck_have_fun()
dist-rtr01 :

dist-rtr02, edge-sw01.
, :

diff_topology.js .var topologyData = {
"links": [
{
"id": 0,
"is_dead": "no",
"is_new": "yes",
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/2"
},
{
"id": 1,
"is_dead": "no",
"is_new": "yes",
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi4",
"target": 1,
"tgtDevice": "dist-sw01.devnet.lab",
"tgtIfName": "Eth1/3"
},
{
"id": 2,
"is_dead": "no",
"is_new": "yes",
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi6",
"target": 0,
"tgtDevice": "dist-rtr02.devnet.lab",
"tgtIfName": "Gi6"
},
{
"id": 3,
"is_dead": "no",
"is_new": "yes",
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi5",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/3"
},
{
"id": 4,
"is_dead": "no",
"is_new": "yes",
"source": 4,
"srcDevice": "dist-rtr01.devnet.lab",
"srcIfName": "Gi2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/2"
},
{
"id": 5,
"is_dead": "no",
"is_new": "no",
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/3"
},
{
"id": 6,
"is_dead": "no",
"is_new": "no",
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi4",
"target": 1,
"tgtDevice": "dist-sw01.devnet.lab",
"tgtIfName": "Eth1/4"
},
{
"id": 7,
"is_dead": "no",
"is_new": "no",
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi5",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/4"
},
{
"id": 8,
"is_dead": "no",
"is_new": "no",
"source": 0,
"srcDevice": "dist-rtr02.devnet.lab",
"srcIfName": "Gi2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/3"
},
{
"id": 9,
"is_dead": "no",
"is_new": "no",
"source": 1,
"srcDevice": "dist-sw01.devnet.lab",
"srcIfName": "Eth1/1",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/1"
},
{
"id": 10,
"is_dead": "no",
"is_new": "no",
"source": 1,
"srcDevice": "dist-sw01.devnet.lab",
"srcIfName": "Eth1/2",
"target": 2,
"tgtDevice": "dist-sw02.devnet.lab",
"tgtIfName": "Eth1/2"
},
{
"id": 11,
"is_dead": "yes",
"is_new": "no",
"source": 7,
"srcDevice": "edge-sw01.devnet.lab",
"srcIfName": "Gi0/2",
"target": 5,
"tgtDevice": "core-rtr01.devnet.lab",
"tgtIfName": "Gi0/0/0/1"
},
{
"id": 12,
"is_dead": "yes",
"is_new": "no",
"source": 7,
"srcDevice": "edge-sw01.devnet.lab",
"srcIfName": "Gi0/3",
"target": 3,
"tgtDevice": "core-rtr02.devnet.lab",
"tgtIfName": "Gi0/0/0/1"
}
],
"nodes": [
{
"icon": "router",
"id": 0,
"is_dead": "no",
"is_new": "no",
"model": "CSR1000V",
"name": "dist-rtr02.devnet.lab",
"serial_number": "9YZKNQKQ566",
},
{
"icon": "switch",
"id": 1,
"is_dead": "no",
"is_new": "no",
"model": "Nexus9000 9000v Chassis",
"name": "dist-sw01.devnet.lab",
"serial_number": "9MZLNM0ZC9Z",
},
{
"icon": "switch",
"id": 2,
"is_dead": "no",
"is_new": "no",
"model": "Nexus9000 9000v Chassis",
"name": "dist-sw02.devnet.lab",
"serial_number": "93LCGCRUJA5",
},
{
"icon": "router",
"id": 3,
"is_dead": "no",
"is_new": "no",
"model": "n/a",
"name": "core-rtr02.devnet.lab",
"serial_number": "n/a",
},
{
"icon": "router",
"id": 4,
"is_dead": "no",
"is_new": "yes",
"model": "CSR1000V",
"name": "dist-rtr01.devnet.lab",
"serial_number": "9S78ZRF2V2B",
},
{
"icon": "router",
"id": 5,
"is_dead": "no",
"is_new": "no",
"model": "n/a",
"name": "core-rtr01.devnet.lab",
"serial_number": "n/a",
},
{
"icon": "unknown",
"id": 6,
"is_dead": "no",
"is_new": "no",
"model": "CSR1000V",
"name": "internet-rtr01.virl.info",
"serial_number": "9LGWPM8GTV6",
},
{
"icon": "dead_node",
"id": 7,
"is_dead": "yes",
"is_new": "no",
"model": "IOSv",
"name": "edge-sw01.devnet.lab",
"serial_number": "927A4RELIGI",
}
]
};
NeXt UI next_app.js.
:
$ python3.7 generate_topology.py
main.html
:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
: dist-rtr01.devnet.lab
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
: edge-sw01.devnet.lab
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
dist-rtr01.devnet.lab(Gi3) core-rtr02.devnet.lab(Gi0/0/0/2)
dist-rtr01.devnet.lab(Gi4) dist-sw01.devnet.lab(Eth1/3)
dist-rtr01.devnet.lab(Gi6) dist-rtr02.devnet.lab(Gi6)
dist-rtr01.devnet.lab(Gi5) dist-sw02.devnet.lab(Eth1/3)
dist-rtr01.devnet.lab(Gi2) core-rtr01.devnet.lab(Gi0/0/0/2)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
edge-sw01.devnet.lab(Gi0/2) core-rtr01.devnet.lab(Gi0/0/0/1)
edge-sw01.devnet.lab(Gi0/3) core-rtr02.devnet.lab(Gi0/0/0/1)
diff_page.html
main.html '
.
NeXt UI
NeXt UI.
nx.graphic.Topology.Link:
nx.define('CustomLinkClass', nx.graphic.Topology.Link, {
properties: {
sourcelabel: null,
targetlabel: null
},
view: function(view) {
view.content.push({
name: 'source',
type: 'nx.graphic.Text',
props: {
'class': 'sourcelabel',
'alignment-baseline': 'text-after-edge',
'text-anchor': 'start'
}
}, {
name: 'target',
type: 'nx.graphic.Text',
props: {
'class': 'targetlabel',
'alignment-baseline': 'text-after-edge',
'text-anchor': 'end'
}
});
return view;
},
methods: {
update: function() {
this.inherited();
var el, point;
var line = this.line();
var angle = line.angle();
var stageScale = this.stageScale();
line = line.pad(18 * stageScale, 18 * stageScale);
if (this.sourcelabel()) {
el = this.view('source');
point = line.start;
el.set('x', point.x);
el.set('y', point.y);
el.set('text', this.sourcelabel());
el.set('transform', 'rotate(' + angle + ' ' + point.x + ',' + point.y + ')');
el.setStyle('font-size', 12 * stageScale);
}
if (this.targetlabel()) {
el = this.view('target');
point = line.end;
el.set('x', point.x);
el.set('y', point.y);
el.set('text', this.targetlabel());
el.set('transform', 'rotate(' + angle + ' ' + point.x + ',' + point.y + ')');
el.setStyle('font-size', 12 * stageScale);
}
}
}
});
topo.
, , .
linkConfig: {
linkType: 'curve',
sourcelabel: 'model.srcIfName',
targetlabel: 'model.tgtIfName',
style: function(model) {
if (model._data.is_dead === 'yes') {
return { 'stroke-dasharray': '5' }
}
},
color: function(model) {
if (model._data.is_dead === 'yes') {
return '#E40039'
}
if (model._data.is_new === 'yes') {
return '#148D09'
}
},
},
NeXt .
. .
:
topo.registerIcon("dead_node", "img/dead_node.png", 49, 49);
diff_page.html :

. ?
.
NeXt UI.
:
- .
- (, NetBox) .
dcimDeviceLink.
. . - IP-, .
nx.ui.Component :
nx.define('CustomNodeTooltip', nx.ui.Component, {
properties: {
node: {},
topology: {}
},
view: {
content: [{
tag: 'div',
content: [{
tag: 'h5',
content: [{
tag: 'a',
content: '{#node.model.name}',
props: {"href": "{#node.model.dcimDeviceLink}"}
}],
props: {
"style": "border-bottom: dotted 1px; font-size:90%; word-wrap:normal; color:#003688"
}
}, {
tag: 'p',
content: [
{
tag: 'label',
content: 'IP: ',
}, {
tag: 'label',
content: '{#node.model.primaryIP}',
}
],
props: {
"style": "font-size:80%;"
}
},{
tag: 'p',
content: [
{
tag: 'label',
content: 'Model: ',
}, {
tag: 'label',
content: '{#node.model.model}',
}
],
props: {
"style": "font-size:80%;"
}
}, {
tag: 'p',
content: [{
tag: 'label',
content: 'S/N: ',
}, {
tag: 'label',
content: '{#node.model.serial_number}',
}],
props: {
"style": "font-size:80%; padding:0"
}
},
],
props: {
"style": "width: 150px;"
}
}]
}
});
nx.define('Tooltip.Node', nx.ui.Component, {
view: function(view){
view.content.push({
});
return view;
},
methods: {
attach: function(args) {
this.inherited(args);
this.model();
}
}
});
topo:
tooltipManagerConfig: {
nodeTooltipContentClass: 'CustomNodeTooltip'
},
:

, 'force' NeXt UI. , .
. , , , .
NeXt UI .
layerSortPreference.
, , .
:
var currentLayout = 'auto'
horizontal = function() {
if (currentLayout === 'horizontal') {
return;
};
currentLayout = 'horizontal';
var layout = topo.getLayout('hierarchicalLayout');
layout.direction('horizontal');
layout.levelBy(function(node, model) {
return model.get('layerSortPreference');
});
topo.activateLayout('hierarchicalLayout');
};
vertical = function() {
if (currentLayout === 'vertical') {
return;
};
currentLayout = 'vertical';
var layout = topo.getLayout('hierarchicalLayout');
layout.direction('vertical');
layout.levelBy(function(node, model) {
return model.get('layerSortPreference');
});
topo.activateLayout('hierarchicalLayout');
};
main.html diff_page.html:
<button onclick='horizontal()'> </button>
<button onclick="vertical()"> </button>
generate_topology.py :
NX_LAYER_SORT_ORDER = (
'undefined',
'outside',
'edge-switch',
'edge-router',
'core-router',
'core-switch',
'distribution-router',
'distribution-switch',
'leaf',
'spine',
'access-switch'
)
def get_node_layer_sort_preference(device_role):
for i, role in enumerate(NX_LAYER_SORT_ORDER, start=1):
if device_role == role:
return i
return 1
NX_LAYER_SORT_ORDER .
: 0() NeXt UI, , undefined , . .
() Nornir .
data :
dist-rtr01:
hostname: 10.10.20.175
platform: ios
groups:
- devnet-cml-lab
data:
role: distribution-router
nr_role, global_facts normalize_result:
global_facts[device_fqdn]['nr_role'] = nr.inventory.hosts[device].get('role', 'undefined')
generate_topology_json :
device_role = facts[host].get('nr_role', 'undefined')
topology_dict['nodes'].append({
'id': host_id,
'name': host,
'primaryIP': device_ip,
'model': device_model,
'serial_number': device_serial,
'layerSortPreference': get_node_layer_sort_preference(
device_role
),
'icon': get_icon_type(
lldp_capabilities_dict.get(host, ''),
device_model
)
})
. :

- GitHub.
:
$ tree . -L 2
.
├── LICENSE
├── README.md
├── diff_page.html
├── diff_topology.js
├── generate_topology.py
├── img
│ └── dead_node.png
├── inventory
│ ├── groups.yml
│ └── hosts_devnet_sb_cml.yml
├── main.html
├── next_app.js
├── next_sources
│ ├── css
│ ├── doc
│ ├── fonts
│ └── js
├── nornir_config.yml
├── requirements.txt
├── samples
│ ├── sample_diff.png
│ ├── sample_layout_horizontal.png
│ ├── sample_link_details.png
│ ├── sample_node_details.png
│ └── sample_topology.png
├── styles_main_page.css
└── topology.js
, , .
本文试图陈述和评论最终版本开发的主要阶段,并回顾所使用的工具。马拉松比赛中的解决方案居首位,最重要的是,它是跨平台的,不仅在实验室中,而且有进一步扩展和集成的潜力。
希望这对某人有帮助。
我将很高兴收到反馈和建设性的批评。有什么可以改变或改进的?
您将如何解决问题,或者您已经如何解决了?