Hallo Habr! Dieser Artikel basiert auf der Lösung der Aufgabe beim letzten DevNet-Online-Marathon von Cisco. Die Teilnehmer wurden gebeten, die Analyse und Visualisierung einer beliebigen Netzwerktopologie und optional die darin auftretenden Änderungen zu automatisieren.
Die Aufgabe ist nicht die trivialste, und in der Blogosphäre gibt es nur wenige Artikel zu diesem Thema. Im Folgenden präsentiere ich eine Diskussion unserer eigenen Implementierung sowie eine Beschreibung der verwendeten Tools und Ansätze.
Alle Interessierten willkommen bei cat!

* Und nicht viel Javascript.
Haftungsausschluss
, .
, .
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
, , .
In dem Artikel wurde versucht, die Hauptphasen der Entwicklung der endgültigen Version anzugeben und zu kommentieren sowie die verwendeten Tools zu überprüfen. Die Lösung beim Marathon stand an erster Stelle und ist vor allem plattformübergreifend und bietet das Potenzial für eine weitere Expansion und Integration, nicht nur im Labor.
Hoffe das kann jemandem helfen.
Ich freue mich über Feedback und konstruktive Kritik. Was könnte geändert oder verbessert werden?
Wie würden Sie sich der Lösung eines Problems nähern oder wie haben Sie es bereits gelöst?