Halo, Habr! Artikel ini ditulis berdasarkan solusi dari tugas di maraton online DevNet terbaru dari Cisco. Peserta diminta untuk mengotomatisasi analisis dan visualisasi topologi jaringan yang sewenang-wenang dan, secara opsional, perubahan yang terjadi di dalamnya.
Tugasnya bukan yang paling sepele, dan di blogosphere ada beberapa artikel tentang topik ini. Di bawah ini saya menyajikan diskusi tentang implementasi kami sendiri, serta deskripsi alat dan pendekatan yang digunakan.
Semua orang yang tertarik, selamat datang ke kucing!

* Dan tidak banyak Javascript.
Penolakan
, .
, .
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
, , .
Artikel tersebut mencoba untuk menyatakan dan mengomentari tahap utama pengembangan versi final, serta meninjau alat yang digunakan. Solusi di maraton terjadi pertama kali dan, yang paling penting, adalah lintas-platform dan memiliki potensi untuk ekspansi dan integrasi lebih lanjut, tidak hanya di laboratorium.
Semoga ini bisa bermanfaat bagi seseorang.
Saya akan senang menerima umpan balik dan kritik yang membangun. Apa yang bisa diubah atau ditingkatkan?
Bagaimana Anda mendekati solusi dari suatu masalah, atau bagaimana Anda sudah menyelesaikannya?