Free Cisco Network Drawing Tool
Hey everyone! This is a survey-up article on a local anesthetic Cisco Russia DevNet Marathon online case I attended in May 2022. IT was a series of informative webinars happening network automation followed by daily challenges based on the discussed topics.
Connected a final day, the participants were challenged to automate a topology analysis and visualization of an arbitrary network segment and, optionally, track and visualize the changes.
The labor was definitely not trivial and non wide covered publicly web log posts. In this article, I would like to go bad my possess solution that at length took first place and draw the selected toolset and considerations.
Let's get started.
Disclaimer
This is a translation of my original article in Russian I posted in Crataegus oxycantha 2022. If you are willing to help to improve the version, please DM me.
The article does non aim to underwrite every the possible scenarios yet does describe the general approaches based happening the specific sheath.
All above and below is an author's personal subjective opinion if not stated otherwise.
All enrolled code is published subordinate Massachusetts Institute of Technology license and does not provide guarantees of some kind.The solution is handwritten in Python and JavaScript. An understanding of the programming and networking basics is desirable for reading.
If you found a typo, please use Ctrl+Enter or ⌘+Enter to post it to the author.
The Task
The final job description was the following:
There is a network consisting of various L2/L3 network devices running IOS/IOS-XE. You consume a list of management IP-addresses of all the devices. All devices are available by their IP addresses. You have access rights to carry through any 'show' commands. You are free to use some information gathering methods. Just trust us, you unlikely need SNMP. We are not ought to limit your fantasy though.The primary task is to identify the physical topology and twist interconnections supported LLDP data and visualize it in a human-couthie format (yeah, we all discover visual diagrams more decipherable). Then you should save the result in a initialise suitable for further analysis by the computing device (yeah, machines are not that good at reading sensory system diagrams).
The topology view should include:
- Contrary icons for to each one device type (routers and switches may have the homophonic icon).
- Twist hostnames.
- Interface names (you may use a shortened formatting, e.g. Gi0/0 for GigabitEthernet0/0/0).
IT is allowed to implement filters limiting or concealment some of the data.
A bonus task is to identify the topology changes (by comparing the present-day and the previous reading) and visualize them in a human-friendly format.
A summary: Informatics-addresses and credentials as an input, a visualised topology as an output (and a great space for experiments and options somewhere in betwixt).
I also denoted some additional physical considerations to follow patc choosing the solution toolset:
- Feature-rich vs Elongate balance.
The solution should be balanced in terms of useable features and the relaxation of their custom and implementation. Some quick-to-use open-root free tools might be used. - Familiarity with the selected toolset.
We have had trine days to complete the task. The world-class day I had to pass on roughly side emergencies. So to be able to provide a working result within such a limited prison term frame some known tools were a must. - Solution reusability.
The chore is potentially applicable to many yield networks. One should keep this in thinker. - Support for multiple network platforms.
Real-world networks consist of numerous platforms. This is a thing to note As symptomless. - The documentation must glucinium available for the chosen toolset.
I believe this is a mandatory essential for any reusable solution.
Existing solutions
It is always a good idea to check whether the bike is already fictional ahead reinventing IT by yourself. I give made about research on getable existing solutions for network visual image. Not surprisingly, no result could fit all the requirements out of the box. Most such solutions were built into much large (and typically outlying non free) enterprise-class network monitoring and analysis systems which would significantly reduce reusability and customization potential.
I would divide an abstract network visualization task into the following layers and steps:
Let's focus on each of them while keeping our essential in mind:
-
Network Equipment
The initial labor requires us to support IOS and IOS-XE.
Actual-world networks lean to be much more heterogeneous. Let's try to reckon this.
-
Topographic anatomy Data Sources
The labor statement suggests us to utilize LLDP (Link Bed Find Protocol) protocol. This is an L2 (OSI Link Layer) protocol described in IEEE 802.1AB standard. It is widely supported by redbrick network platforms (including IOS and IOS-XE) and operating systems (including Linux and Window), and then it suits our case.
The network topology data can too be enriched by various outputs from the meshwork devices, such as routing and switching tables, routing protocols data, and so on. Let's fair-minded cross out it for future improvements.
-
Data Access Protocols and Standards
The most modern platforms usually support vitreous and chrome NETCONF, REST APIs, RESTCONF with YANG models and data structures. The existence of legacy equipment and platforms will usually storm us to revert to SSH, Telnet, and good ol' CLI.
-
Protocol- and Vendor-Specific Drivers or Plugins
The core logic of the solution will be written in Python because of two primary reasons: Python has a pretty comprehensive set of handy modules and libraries for network mechanisation and this is a programing language I am most experienced in.
API-ambitious network automation in Python stool be done victimization the requests mental faculty operating theatre both specialized modules.
Naked SSH/Telnet get at to network equipment from Python commonly relies on netmiko, paramiko, or scrapli modules. They let you emulate the standard CLI: sending roughly text commands to the session and expecting back the text output of the Sir Thomas More or inferior predictable readability level and formatting.
There are also several high-level Python frameworks allowing for additional features connected whirligig of the tools I mentioned above. The two all but useful of them in our shell are NAPALM and Nornir. NAPALM provides vendor-neutral GETTERs for getting structured data from the meshwork devices. Nornir implements many useful abstractions and multithreading out of the box.
Eastern Samoa for SNMP, let's leave it for network monitoring purposes.
-
Unstructured Data -> Data Normalization Toolset -> Structured Data
Data gathering with API usually allows you to get structured output full-strength away. A text output you get from network equipment CLI is natively inapplicable for further machine processing. A traditional way to extract the data from the CLI output in Python is Ra mental faculty and nightly expressions. Modern approaches are TextFSM framework highly-developed past Google and brand new TTP (Template Text Parser) developed by dmulyalin. Some tools perform data parsing with more usable templates in comparability to regular expressions.
NAPALM faculty mentioned above performs ambiguous data standardization internally for supported GETTERs and returns the organized yield. This Crataegus laevigata do things some easier in our case.
-
Data Processing and Analysis -> Topology Representation in Python Datastructures
One time we get the structured topology information pieces from all our devices, all we need to do is to bring it to common agency, analyze and assemble a final puzzle.
-
Topology Representation in Visualisation Engine Initialise
Depending on visualization locomotive engine extract, you Crataegus laevigata take to transform a final network topology data format reported to what the tool supports as an input.
-
Visualization Engine
This point was the most not manifest for ME and I had no prior experience in such development. Google search and discussions in DevNet Marathon telegram channel with colleagues introduced me to several Python (pygraphviz, matplotlib, networkx) and JavaScript (JS D3.js, vis.js.) frameworks. And then I found JavaScript+HTML5 NeXt UI Toolkit I once bookmarked while digging through Cisco DevNet labs in front. This is a specialized network visual image toolkit developed aside Lake herring. It has many features and proper documentation.
-
Envisioned Topographic anatomy
Our final examination goal. A view may vary from a unsophisticated static image or an Hypertext mark-up language document to something Sir Thomas More advanced and interactive.
Hera is a summary of the most common tools we have:
Based on the requirements preceding I have selected the following tools for my target resolution:
- LLDP is a topology data source.
- SSH and CLI for fundamental interaction with the network devices.
- Nornir for multithreading, more useful data gathering solution handling and processing, and keeping the information about our devices in a structured Stock-taking.
- NAPALM to abstract from hand-operated CLI scrapping.
- Python3 for writing the core logic.
- Incoming UI (JS+HTML5) for topology visual image based on result we get from Python peace of encode.
I have already used NAPALM and Nornir with success for network audits and data gather from hundreds of network devices before. Default NAPALM GETTERs support LLDP on Coregonus artedi IOS/IOS-XE, IOS-XR, NX-Operating system, Juniper JunOS, and Arista EOS.
Furthermore, the system of logic separation discussed above would allow us to add more data sources and mesh connectors without affecting the whole codebase.
Next UI was a thing to acquaint with and work out how IT works on the run. However, the examples looked promising.
Preparation
Test lab
I used Cisco Model Labs as a run lab. This is a new version of the VIRL network emulator. Cisco DevNet Sandbox allows using it for free for a limited time window. You just have to register and proceed with a reservation which is a matter of a few mouse clicks (and a few more to connect to the lab victimisation AnyConnect VPN once you undergo the reservation details dorsum to your electronic mail). In the old days we would have to use up
a bare bronze homelab or to have fun with GNS3.
Lab topology on the CML web user interface looks as follows (we should get the similar picture as solution):
It consists of Lake herring devices of any kind: IOS (edge in-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02), NXOS (dist-sw01, dist-sw02), IOSXR (core-rtr01, core-rtr02), ASA (edge-firewall01). LLDP is enabled along all of them. SSH admittance is available on IOS, IOSXE, and NXOS nodes.
Installment and initializing Nornir
Nornir is an ASCII text file Python framework. Information technology is available connected PyPI for Python 3.6.2 and higher up. Nornir has a dozen of dependencies, including NAPALM and netmiko. IT is recommended to use Python virtual environments (venv to isolate the dependencies. My local anaesthetic development environment in use Nornir 2.4.0 with Python 3.7 happening MacOS 10.15. This should work Linux and Windows as well. Nornir installation is unequivocal:
$ mkdir ~/testenv $ python3.7 -m venv ~/testenv/ $ source ~/testenv/bin/activate (testenv)$ pip install nornir==2.4.0 Authoritative: Nornir has had some massive changes in the 3.X release. Some of them were not backward compatible with 2.X versions. Nornir related configurations and code are relevant to 2.X versions.
Nornir supports single inventory plugins. They all provide a convenient way to structure and operate your network devices' information programmatically. For this solution, the standard SimpleInventory plugin is sufficient.
General Nornir settings are listed in a set of YAML files. Configuration file names can be arbitrary but you should point Nornir to their exact names during initialization from Python.
nornir_config.yaml:
--- core: num_workers: 20 inventory: plugin: nornir.plugins.stock-take.simple.SimpleInventory options: host_file: "inventory/hosts_devnet_sb_cml.yml" group_file: "inventory/groups.yml" A sample Nornir elementary shape file you can see above contains references to two to a greater extent YAML files: hosts file and groups Indian file. These files delimit the SimpleInventory plugin configuration. The Hosts file contains a lean of our network devices (hosts) with their attributes. Groups file contains a list of groups and their attributes. An individual host can exist included in one or Sir Thomas More groups. The host inherits the attributes of every groups information technology belongs to. Hosts and groups file names and locations posterior be arbitrary as well.
inventory/hosts_devnet_sb_cml.yml has the following structure:
--- net-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 Just two hosts are organism displayed for brevity. Both hosts make Information science-address, platform attributes. dist-sw01 has transport type assigned specifically. For cyberspace-rtr01, transport type will be chosen based happening platform typecast (it is SSH for IOS by default) aside Nornir. Both hosts consist to the 'devnet-cml-lab' group.
groups.yml will define all group settings for them:
--- devnet-cml-lab: username: cisco watchword: cisco connection_options: napalm: extras: optional_args: secret: cisco Group attributes above contain access credentials and enable enigma for Cisco devices. These attributes will embody inherited by all group members.
Important: Ne'er store credential (and any sensitive data) in your production environment in clear text configs corresponding this. This simple config is used for demonstrational and lab purposes only.
Those are complete general Nornir configuration steps. All we deman to do straightaway is to initialize it from a Python code.
Downloading NeXt UI
For local usage and examination, it is enough to download the NeXt UI source code from GitHub. Countenance's put the sources into ./next_sources in our project stem directory.
Our progress upon download completion:
$ tree . -L 2 . ├── take stock │ ├── groups.yml │ └── hosts_devnet_sb_cml.yml ├── next_sources │ ├── css │ ├── doc │ ├── fonts │ └── js ├── nornir_config.yml Long time of Topology Discovery
The main logic will be written in a Python script named generate_topology.py.
Initializing Nornir
Once our Nornir config is ready, it can be initialized in Python as simply as:
from nornir import InitNornir from nornir.plugins.tasks.networking spell napalm_get NORNIR_CONFIG_FILE = "nornir_config.yml" nr = InitNornir(config_file=NORNIR_CONFIG_FILE) That's information technology. Nornir is ready to work.
napalm_get imported above allows U.S.A to use NAPALM straight from Nornir.
LLDP at-a-glance
LLDP-enabled devices commute periodic LLDP messages consisting of TLV-fields with their direct neighbors. LLDP messages are non normally relayed.
Mandatory TLV fields: Frame ID, Port ID, Time-to-Live.
Optional TLV fields: Organisation Public figure and Description; Port Nominate and Verbal description; VLAN Name; IP Management address; System Capabilities (switch, routing, etc.), and more.
As the examined topology segment is under our control, let's consider System Name and Port Name TLV Fields mandatory and advertisable internally.
Information technology does not cause epoch-making security risks but allows us to identify multi-chassis devices with a shared control even (e.g. stacked switches) and device interconnections uniquely.
Therein case, a topology psychoanalysis task American Samoa a whole can constitute slashed to the analysis of the neighborship data received on each device. This allows us to name unique devices and their interconnections (i.e. Vertices and Edges of the network topology Graph).
By the way, OSPF LSA switch over and analysis work in a real similar way of life. Visualizing routing communications protocol data may besides be a good use case (I'd recommend to check out the Topolograph table service released in October 2022 by @Vadims06). Only rent's cente LLDP for now.
In our lab environment, all edge, core, and distribution layer devices should see their direct LLDP neighbors. internet-rtr01 is isolated from the rest of the network so it should not rich person some LLDP neighbors.
Here is a manual " usher lldp neighbors" output from dist-rtr01:
dist-rtr01#show lldp neighbors Capability codes: (R) Router, (B) Bridge circuit, (T) Telephone, (C) DOCSIS Cable television Gimmick (W) Wireless local area network Access Point, (P) Repeater, (S) Station, (O) Other Device ID Local anesthetic Intf Hold-time Capability Interface 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 Five neighbors. Looks good.
The same output from core-rtr02:
RP/0/0/CPU0:gist-rtr02#show lldp neighbors Sun May 10 22:07:05.776 UTC Potentiality codes: (R) Router, (B) Span, (T) Telephone, (C) DOCSIS Cable Device (W) WiFi Access code Power point, (P) Repeating firearm, (S) Station, (O) Other Gimmick ID Local anesthetic Intf Hold-time Capability Port I.D. core-rtr01.devnet.La Gi0/0/0/0 120 R Gi0/0/0/0 butt-sw01.devnet.lab Gi0/0/0/1 120 R Gi0/3 dist-rtr01.devnet.lanthanum Gi0/0/0/2 120 R Gi3 dist-rtr02.devnet.la Gi0/0/0/3 120 R Gi3 Total entries displayed: 4 Four neighbors. That's correct as well.
Please note that the output contains uncomplete hostnames in a Device Gem State column in both cases.
CLI automation ever comes along with much issues.
In our inclined sheath the workaround is to use a detailed output data format.
As an example:
show lldp neighbors detail' from IOSXE-based dist-rtr01
dist-rtr01#show lldp neighbors detail ------------------------------------------------ Localized Intf: Gi6 Chassis id: 001e.e57c.cf00 Port I.D.: Gi6 Port Description: L3 Link to dist-rtr01 System Diagnose: dist-rtr02.devnet.science lab System of rules Description: Cisco IOS Software [Gibraltar], Virtual Xenon Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE SOFTWARE (fc2) Technical Support: http://www.Coregonus artedi.com/techsupport Copyright (c) 1986-2022 by Cisco Systems, Iraqi National Congress. Compiled Tue 28-May-19 12:45 Fourth dimension remaining: 91 seconds System Capabilities: B,R Enabled Capabilities: R Management Addresses: IP: 172.16.252.18 Auto Dialogue - not supported Physical media capabilities - not advertised Media Attachment Unit eccentric - not advertised Vlan ID: - not advertised ------------------------------------------------ Local Intf: Gi4 Chassis I.D.: 5254.0007.5d59 Port Gem State: Ethernet1/3 Embrasure Description: L3 link to dist-rtr01 System Identify: dist-sw01.devnet.lab System Verbal description: Cisco Nexus Operational System (NX-OS) Software 9.2(3) TAC financial backing: http://www.Coregonus artedi.com/tac Right of first publication (c) 2002-2022, Cisco Systems, Inc. All rights reserved. Sentence leftover: 108 seconds System Capabilities: B,R Enabled Capabilities: B,R Management Addresses: IP: 10.10.20.177 Different: 52 54 00 07 5D 59 00 Auto Negotiation - not supported Physical media capabilities - not advertised Media Attachment Unit typewrite - not advertised Vlan ID: - non advertised ------------------------------------------------ Local Intf: Gi5 Chassis id: 5254.0007.b7e6 Port ID: Ethernet1/3 Port Description: L3 link to dist-rtr01 Organization Name: dist-sw02.devnet.lab System Description: Cisco Link OS (NX-OS) Package 9.2(3) TAC support: http://www.cisco.com/tac Right of first publication (c) 2002-2022, Coregonus artedi Systems, INC. All rights restrained. Time remaining: 97 seconds Scheme Capabilities: B,R Enabled Capabilities: B,R Direction Addresses: Informatics: 10.10.20.178 Unusual: 52 54 00 07 FF FF 00 Automobile Dialogue - not founded Physical media capabilities - not publicised Media Affixation Unit type - not publicized Vlan ID: - not advertised ------------------------------------------------ Local Intf: Gi3 Chassis id: 02c7.9dc0.0c06 Port id: Gi0/0/0/2 Porthole Description: L3 Link to dist-rtr01 System Refer: core-rtr02.devnet.laboratory System Description: Coregonus artedi IOS XR Software, Version 6.3.1[Default option] Copyright (c) 2022 aside Coregonus artedi Systems, Inc., IOS XRv Series Time leftover: 94 seconds System Capabilities: R Enabled Capabilities: R Direction Addresses: IP: 172.16.252.26 Auto Negotiation - non supported Physical media capabilities - not advertised Media Attachment Unit type - not advertised Vlan ID: - not publicized ------------------------------------------------ Section Intf: Gi2 Physique 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: Coregonus artedi IOS XR Software, Version 6.3.1[Default] Copyright (c) 2022 by Cisco Systems, INC., IOS XRv Series Time left: 110 seconds System Capabilities: R Enabled Capabilities: R Management Addresses: Information processing: 172.16.252.22 Machine 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 from NXOS-settled dist-sw01
dist-sw01# show lldp neighbors detail Capability codes: (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device (W) Wireless fidelity Access Point, (P) Repeating firearm, (S) Station, (O) Other Device ID Local Intf Hold-clock 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.research lab System Description: Cisco Nexus Operating System (NX-Atomic number 76) Software 9.2(3) TAC support: HTTP://WWW.cisco.com/tac Copyright (c) 2002-2022, Cisco Systems, Inc. Entirely rights reserved. Time remaining: 112 seconds System Capabilities: B, R Enabled Capabilities: B, R Direction Speak: 10.10.20.178 Management Address IPV6: not advertised Vlan ID: 1 Chassis id: 5254.0007.b7e5 Port wine id: Ethernet1/2 Local Port id: Eth1/2 Port Verbal description: VPC Match Link System Name: dist-sw02.devnet.lab Arrangement Description: Cisco Nexus Operating System (NX-Operating system) Package 9.2(3) TAC support: HTTP://web.cisco.com/tac Copyright (c) 2002-2022, Cisco Systems, Iraqi National Congress. All rights engaged. Time left over: 112 seconds Organization Capabilities: B, R Enabled Capabilities: B, R Management Address: 10.10.20.178 Management Address IPV6: not publicized 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 Scheme Description: Coregonus artedi IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, RELEASE Software program (fc2) Technical Support: http://web.cisco.com/techsupport Copyright (c) 1986-2022 by Cisco Systems, Iraqi National Congress. Compiled Tue 28-May-19 12:45 Time remaining: 109 seconds System Capabilities: B, R Enabled Capabilities: R Direction Address: 172.16.252.2 Management Address IPV6: non advertised Vlan ID: not advertised Form id: 001e.e57c.cf00 Port id: Gi4 Topical anaestheti Port id: Eth1/4 Port Description: L3 Link to dist-sw01 System Advert: dist-rtr02.devnet.lab System of rules Description: Cisco IOS Software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, Resign SOFTWARE (fc2) Technical Support: http://web.Coregonus artedi.com/techsupport Copyright (c) 1986-2022 by Cisco Systems, Inc. Compiled Tue 28-English hawthorn-19 12:45 Meter left: 108 seconds Arrangement Capabilities: B, R Enabled Capabilities: R Management Address: 172.16.252.6 Management Savoir-faire IPV6: not advertised Vlan I.D.: not advertised Total entries displayed: 4 Gathering the data from the devices
We are going to gather the LLDP data from the devices running IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02), and NXOS (dist-sw01, dist-sw02).
IOS-XR-based core routers (heart-rtr01, core-rtr02) will be advisedly restricted from management access code.
Thereby the following scenarios will be covered:
- Pregnant mesh neighborship handling for whol distribution layer devices.
All unequaled nodes and links should make up ascertained decently. - Device access or connectivity issues handling for core-rtr01 and core-rtr02.
This should non affect the ability to work with the rest of the devices. - Construction the topology settled on partial information from discontiguous network segments.
Both edge switch and statistical distribution routers "see" core-rtr01 and core-rtr02 from different sides.
This should be enough to build the full picture.
Weighed down inventory/hosts_devnet_sb_cml.yml hosts file content
--- internet-rtr01: hostname: 10.10.20.181 platform: ios site: devnet_sandbox groups: - devnet-cml-lab edge-sw01: hostname: 10.10.20.172 program: ios internet site: devnet_sandbox groups: - devnet-cml-lab heart-rtr01: # Device access is restricted for the trial hostname: 10.10.20.173 platform: iosxr groups: - devnet-cml-lab core-rtr02: # Twist access is restricted for the test hostname: 10.10.20.174 platform: iosxr groups: - devnet-cml-lab dist-rtr01: hostname: 10.10.20.175 platform: ios groups: - devnet-cml-science laborator dist-rtr02: hostname: 10.10.20.176 platform: ios groups: - devnet-cml-lab dist-sw01: hostname: 10.10.20.177 platform: nxos_ssh enthral: ssh groups: - devnet-cml-lab dist-sw02: hostname: 10.10.20.178 political platform: nxos_ssh ecstasy: ssh groups: - devnet-cml-lab NAPALM GETTERs to use:
- GET_LLDP_NEIGHBORS_DETAILS.
A careful end product variation is chosen atomic number 3 it provides more consonant data. - GET_FACTS.
It collects some extended device information such as FQDN, sit, serial number, etc.
Let's wrap the data gathering task into a Nornir Task run.
This is one of useful method of grouping actions on the individual hosts.
def get_host_data(task): """Nornir Labor for data collection on target hosts.""" task.run( task=napalm_get, getters=['facts', 'lldp_neighbors_detail'] ) Now we can work the Chore and save the result into variable for a encourage processing.
Default execution range is all devices.
get_host_data_result = nr.bleed(get_host_data) You may also function simple and complex stock list filters to limit the execution scope to individual hosts or groups.
Processing the information received from devices
get_host_data_result variable contains a get_host_data task execution results for each target gimmick.
>>> get_host_data_result AggregatedResult (get_host_data): {'net-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'border-sw01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'kernel-rtr01': MultiResult: [Result: "get_host_data", Result: "napalm_get"], 'core-rtr02': MultiResult: [Final result: "get_host_data", Result: "napalm_get"], 'dist-rtr01': MultiResult: [Final 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"]} All host result object has failed method returning a Boolean value. False means no errors occured during task capital punishment happening a specific host.
The global task result is iterable equally a dictionalry objective:
>>> for device, result in get_host_data_result.items(): ... print(f'{device} failed: {result.failed}') ... internet-rtr01 failed: Faux edge-sw01 failed: False substance-rtr01 failed: Admittedly core-rtr02 failed: Correct dist-rtr01 failed: Treasonably dist-rtr02 failed: False dist-sw01 failing: Sour dist-sw02 failed: Insincere Looks expectedly.
Some complete result outputs for the reference:
Result object content for 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), Translation 16.11.1b, Button Computer 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': 'Coregonus artedi IOS Software [Gibraltar], Virtual XE Package (X86_64_LINUX_IOSD-UNIVERSALK9-M), Translation 16.11.1b, RELEASE SOFTWARE (fc2)', 'remote_system_capab': ['bridge over', '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 OS (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 inter-group communication to dist-rtr01', 'remote_system_name': 'dist-sw02.devnet.laboratory', 'remote_system_description': 'Lake herring Nexus Operative System (NX-Bone) 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 Connec to dist-rtr01', 'remote_system_name': 'meat-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Software system, 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': 'gist-rtr01.devnet.laboratory', 'remote_system_description': 'Cisco IOS XR Software, Variation 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}} Result physical object content for dist-sw01
>>> get_host_data_result['dist-sw01'][1].result {'facts': {'uptime': 6090, 'vendor': 'Lake herring', '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 Organization (NX-Atomic number 76) 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 Join', 'remote_system_name': 'dist-sw02.devnet.lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software program 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], Realistic Atomic number 54 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 Package [Rock of Gibraltar], Virtual XE Software package (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, Loss SOFTWARE (fc2)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]}} Termination object is a dictionary with a keys matching selected GETTER names: 'facts' and 'lldp_neighbors_detail'.
Dictionary values contain a structured data processed and returned by NAPALM.
Let's compare a neighbor sets:
LLDP neigbors of 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.laboratory', 'remote_system_description': 'Cisco IOS Software [Gibraltar], Virtual XE Package (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1b, Firing SOFTWARE (fc2)', 'remote_system_capab': ['span', 'router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]) ('GigabitEthernet4', [{'remote_chassis_id': '5254.0007.5d59', 'remote_port': 'Ethernet1/3', 'remote_port_description': 'L3 radio link to dist-rtr01', 'remote_system_name': 'dist-sw01.devnet.laboratory', 'remote_system_description': 'Cisco Nexus Operative System of rules (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 OS (NX-OS) Software 9.2(3)', 'remote_system_capab': ['span', '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 Connexion to dist-rtr01', 'remote_system_name': 'core-rtr02.devnet.lab', 'remote_system_description': 'Cisco IOS XR Package, Translation 6.3.1[Nonremittal]', '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': 'Coregonus artedi IOS XR Software, Version 6.3.1[Default]', 'remote_system_capab': ['router'], 'remote_system_enable_capab': ['router'], 'parent_interface': ''}]) LLDP neigbors of dist-sw01
>>> for neighbor in get_host_data_result['dist-sw01'][1].result['lldp_neighbors_detail'].items(): ... print(neighbor) ... photographic print('\n') ... ('Ethernet1/1', [{'remote_chassis_id': '5254.0007.b7e4', 'remote_port': 'Ethernet1/1', 'remote_port_description': 'VPC Peer Nexus', 'remote_system_name': 'dist-sw02.devnet.science lab', 'remote_system_description': 'Cisco Nexus Operating System (NX-OS) Software 9.2(3)', 'remote_system_capab': ['bridge', 'router'], 'remote_system_enable_capab': ['bridge circuit', '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 OS (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.laboratory', 'remote_system_description': 'Cisco IOS Computer software [Gibraltar], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Variant 16.11.1b, Exit 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 Associate 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': ''}]) Phoebe neighbors for dist-rtr01 and four neighbors for dist-sw01 which is exactly what we saw in CLI outputs ahead.
The sleep of the data is also valid.
For informality of processing let's split LLDP and facts data into fall apart entities.
Any devices may be also present in multiple outputs. To distinguish between them, IT is necessary to use some singular node identifiers. Let's choose it in the following descending lodg:
- Device FQDN if available (may make up advance referred to as hostname for simplicity).
- Device hostname if usable.
- Device host object key out from Nornir Stocktaking.
LLDP relies on the prime two steps too.
def normalize_result(nornir_job_result): """ get_host_data result parser. Returns LLDP and FACTS data dicts with hostname keys. """ global_lldp_data = {} global_facts = {} for device, output in nornir_job_result.items(): if output[0].failed: # Write default data to dicts if the chore is failed. # Use the host inventory object epithet as a key. global_lldp_data[twist] = {} global_facts[twist] = { 'nr_ip': nr.stock-taking.hosts[device].receive('hostname', 'n/a'), } continue # Use FQDN every bit unparalleled ID for devices withing the script. device_fqdn = output[1].result['facts']['fqdn'] if not device_fqdn: # If FQDN is not settled use hostname. # LLDP TLV follows the same logic. device_fqdn = yield[1].event['facts']['hostname'] if non device_fqdn: # Use master of ceremonies inventory aim name as a key if # neither FQDN nor hostname are set device_fqdn = device global_facts[device_fqdn] = output[1].result['facts'] # People device facts with its IP address or hostname arsenic per Inventory data global_facts[device_fqdn]['nr_ip'] = nr.inventorying.hosts[device].get('hostname', 'n/a') global_lldp_data[device_fqdn] = output[1].resolution['lldp_neighbors_detail'] return global_lldp_data, global_facts Then we should express a tilt of all neighbors known by devices and physique supported that:
- A leaning of unique hosts.
- A list of unequaled golf links between them.
To secure unambiguous identification of links, we will store them in a following format:
((source_device_id, source_port_name), (destination_device_id, destination_port_name))
It is also necessary to support in mind that:
- A link may be seeable from two sides if we collect the data from both devices it is connected to.
We have to check for side A and side B permutations to filter the duplicates. - A interface name may be formatted differently in the LLDP declaration and local outputs. For representative, GigabitEthernet4 locally and Gi4 in LLDP Port discover.
To ensure information body, we will translate the port wine names into a full initialise for the analysis degree. At the same time, let's implement a appoint shortening occasion to provide better visual experience during visual image.
Automatic pistol icon selection can personify implemented supported device capabilities advertised in LLDP. Let's pull out them into a separate {"hostname": "primary_capability"} dictionary.
The same code wise:
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): proceeds ifname.replace(k, v) reelect ifname def if_shortname(ifname): for k, v in interface_full_name_map.items(): if ifname.startswith(v): recall ifname.supplant(v, k) return ifname def extract_lldp_details(lldp_data_dict): """ LLDP data dict parser. Returns localize of all the discovered hosts, LLDP capabilities dict with all LLDP-discovered host, and all discovered interconnections between hosts. """ 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 neighbour['remote_system_name']: continue discovered_hosts.add(neighbor['remote_system_name']) if neighbour['remote_system_enable_capab']: # In cause of multiple enable capabilities pick first in the list lldp_capabilities_dict[neighbor['remote_system_name']] = ( neighbor['remote_system_enable_capab'][0] ) else: lldp_capabilities_dict[neighbor['remote_system_name']] = '' # Store interconnections in a following format: # ((source_hostname, source_port), (dest_hostname, dest_port)) local_end = (host, interface) remote_end = ( neighbor['remote_system_name'], if_fullname(neighbor['remote_port']) ) # Check if the link is not a permutation of already added matchless # (local_end, remote_end) equals (remote_end, local_end) 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.add(( (host, interface), (neighbour['remote_system_name'], if_fullname(neighbour['remote_port'])) )) getting even [discovered_hosts, global_interconnections, lldp_capabilities_dict] Initializing Incoming UI coating
Topology visualization logic will be implemented in next_app.js script based connected Next UI.
Lashkar-e-Tayyiba's start with the basics:
(function (nx) { /** * NeXt UI based application */ // Initialize topology var topo = new nx.vivid.Topology({ // Persuasion dimensions breadth: 1200, height: 700, // Dataprocessor is responsible for spreading // the Nodes crossways the view. // 'force' data processor spreads the Nodes so // they would exist arsenic distant from each other // as accomplishable. Follow social distancing and stay healthy. // 'quick' dataprocessor picks haphazard positions // for the Nodes. dataProcessor: 'military force', // Node and Link identity key dimension name identityKey: 'id', // Node settings nodeConfig: { pronounce: 'model.name', iconType:'model.icon', }, // Link settings linkConfig: { // Display Links as curves in case of // multiple links between Node Pairs. // Set to 'symmetric' to employment parallel links. linkType: 'curved shape', }, // Display Node icon. Displays a acid if set to 'false'. showIcon: true, }); var Shell = nx.define(nx.ui.Application, { methods: { start: function () { // Read topology data from variable topo.data(topologyData); // Attach it to the papers topo.tie(this); } } }); // Produce an applications programme instance var shell = new Eggshell(); // Run the application shell.start(); })(nx); The topology data structure leave be stored in a topologyData variable. Lease's move information technology into a separate topology.js filing cabinet. The format details will be discussed below.
A final visualization consequence will be displayed in a local HTML build with attached JS components:
<!DOCTYPE html> <html> <heading> <meta charset="utf-8"> <inter-group communication rel="stylesheet" href="next_sources/css/incoming.css"> <link rel="stylesheet" href="styles_main_page.css"> <script src="next_sources/js/close.js"></script> <playscript src="topology.js"></book> <book src="next_app.js"></book> </head> <body> </torso> </html> Generating NeXt UI topology in Python
We receive already written obligatory information assemblage result handlers and normalized it exploitation Python data structures.
Let's apply this:
GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result) TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA) General NeXt UI topology representation looks arsenic follows:
// Two nodes on with ii golf links var topologyData = { "links": [ { "id": 0, "source": 0, "poin": 1, }, { "id": 1, "rootage": 0, "target": 1, } ], "nodes": [ { "icon": "router", "id": 0, }, { "icon": "router", "ID": 1, } ] As you tush see, this is a JSON object which tin be mapped to a Python data structure of the following data formatting: {'nodes': [], 'links': []}.
We will bring all our information together into that.
To boot, let's take device model into account for node icon selection to palm lost LLDP capabilities case.
Node objects will also be populated with some extended attributes derived from GET_FACTS (pattern, S/N, etc) to enrich the topology view.
icon_capability_map = { 'router': 'router', 'permutation': 'change over', 'bridge': 'switch', 'station': 'master of ceremonies' } 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': 'switching', } def get_icon_type(device_cap_name, device_model=''): """ Device icon extract function. Survival of the fittest order: - LLDP capabilities mapping. - Device model mapping. - Default 'unknown'. """ if device_cap_name: icon_type = icon_capability_map.get(device_cap_name) if icon_type: return icon_type if device_model: # Check substring presence in icon_model_map keys # bowed stringed instrument until the first cope with for model_shortname, icon_type in icon_model_map.items(): if model_shortname in device_model: counte icon_type return 'unexplored' def generate_topology_json(*args): """ JSON topology object generator. Takes as an input signal: - determined hosts set, - LLDP capabilities dict with hostname keys, - interconnections listing, - facts dict with hostname keys. """ discovered_hosts, interconnections, lldp_capabilities_dict, facts = args host_id = 0 host_id_map = {} topology_dict = {'nodes': [], 'golf links': []} for host in discovered_hosts: device_model = 'n/a' device_serial = 'n/a' device_ip = 'n/a' if facts.fetch(server): 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': emcee, '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 data link in interconnections: topology_dict['golf links'].append({ 'id': link_id, 'seed': host_id_map[link[0][0]], 'target': host_id_map[connec[1][0]], 'srcIfName': if_shortname(tie in[0][1]), 'srcDevice': link[0][0], 'tgtIfName': if_shortname(link[1][1]), 'tgtDevice': link[1][0], }) link_id += 1 return topology_dict Then we should just write this Python regional anatomy dictionary into the topology.js file. A regular json module will work for this absolutely providing a readable and formatted turnout:
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') every bit 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) Resulting regional anatomy.js file mental object
var topologyData = { "golf links": [ { "id": 0, "source": 7, "srcDevice": "edge-sw01.devnet.lab", "srcIfName": "Gi0/2", "target": 5, "tgtDevice": "burden-rtr01.devnet.science laborator", "tgtIfName": "Gi0/0/0/1" }, { "id": 1, "source": 7, "srcDevice": "edge-sw01.devnet.lab", "srcIfName": "Gi0/3", "target": 3, "tgtDevice": "core-rtr02.devnet.research lab", "tgtIfName": "Gi0/0/0/1" }, { "id": 2, "source": 4, "srcDevice": "dist-rtr01.devnet.lab", "srcIfName": "Gi3", "target": 3, "tgtDevice": "core-rtr02.devnet.science laborator", "tgtIfName": "Gi0/0/0/2" }, { "Idaho": 3, "reference": 4, "srcDevice": "dist-rtr01.devnet.lab", "srcIfName": "Gi4", "target": 1, "tgtDevice": "dist-sw01.devnet.lab", "tgtIfName": "Eth1/3" }, { "id": 4, "seed": 4, "srcDevice": "dist-rtr01.devnet.lab", "srcIfName": "Gi6", "prey": 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" }, { "I.D.": 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 area": 3, "tgtDevice": "core-rtr02.devnet.lab", "tgtIfName": "Gi0/0/0/3" }, { "id": 8, "source": 0, "srcDevice": "dist-rtr02.devnet.lab", "srcIfName": "Gi4", "poin": 1, "tgtDevice": "dist-sw01.devnet.lab", "tgtIfName": "Eth1/4" }, { "id": 9, "source": 0, "srcDevice": "dist-rtr02.devnet.laboratory", "srcIfName": "Gi5", "target": 2, "tgtDevice": "dist-sw02.devnet.lab", "tgtIfName": "Eth1/4" }, { "id": 10, "source": 0, "srcDevice": "dist-rtr02.devnet.lab", "srcIfName": "Gi2", "aim": 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", "Idaho": 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" }, { "picture": "switch", "id": 1, "model": "Nexus9000 9000v Build", "identify": "dist-sw01.devnet.lab", "serial_number": "9MZLNM0ZC9Z", }, { "picture": "transposition", "id": 2, "model": "Nexus9000 9000v Anatomy", "nominate": "dist-sw02.devnet.lab", "serial_number": "93LCGCRUJA5", }, { "ikon": "router", "id": 3, "model": "n/a", "name": "core-rtr02.devnet.science lab", "serial_number": "n/a", }, { "icon": "router", "id": 4, "model": "CSR1000V", "name": "dist-rtr01.devnet.research laboratory", "serial_number": "9S78ZRF2V2B", }, { "picture": "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.information", "serial_number": "9LGWPM8GTV6", }, { "picture": "flip", "id": 7, "model": "IOSv", "identify": "bound-sw01.devnet.research laboratory", "serial_number": "927A4RELIGI", } ] }; Now let's finally run chief.html and visualize our visualization Hello World:
Looks correct. All known nodes and links between them are displayed.
The nodes are draggable in any direction. Happening mouse click on nodes and links, a Side by side UI tooltip menu appears. IT contains all the attributes we previously passed into node and data link topology objects in Python:
Non so bad. There is also room for advance. We will return to this some later on. Let's put through a solution for the instant part of the task for now.
Detecting and visualizing topology changes
A fillip task is to detect and visualize topology changes.
To get it done, close to additions wish be required:
- A topology stash file cached_topology.js to store a previous topology state.
generate_topology.py script will be reading this lay away single file on each run and rewriting with a newer state if necessity. - diff_topology.js topology file to store a diff topology.
- diff_page.html page to presentation a unreal topology diff.
Diff HTML-form will look as follows:
<!DOCTYPE HTML> <html> <mind> <meta charset="utf-8"> <link rel="stylesheet" href="next_sources/css/future.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>Display current network topology</button></a> </p> </body> </hypertext mark-up language> Altogether we demand to learn and write topology memory cache files:
CACHED_TOPOLOGY_FILENAME = 'cached_topology.json' def write_topology_cache(topology_json, dst=CACHED_TOPOLOGY_FILENAME): with ingenuous(dst, 'w') as cached_file: cached_file.write(json.dumps(topology_json, indention=4, sort_keys=True)) def read_cached_topology(filename=CACHED_TOPOLOGY_FILENAME): if not os.path.exists(filename): return {} if not os.track.isfile(filename): return {} cached_topology = {} with loose(filename, 'r') as file: try: cached_topology = json.loads(file.read()) except: return {} return cached_topology Topology diff analysis steps:
-
Evoke the node and link attributes for comparison from the current and cached topology dictionaries.
Node arrange:
(node object with all attributes, (hostname,))
Link format:
(link object with all attributes, (source_hostnme, source_port), (dest_hostname, dest_port))
Both node and link format allows encourage extension.
-
Compare the extracted node and link objects. Link format permutations should be reasoned.
Diff result for nodes and links volition glucinium written into two dictionaries in the pursuit arrange:
- diff_nodes = {'added': [], 'deleted': []}
- diff_links = {'added': [], 'deleted': []}
-
Merge current and cached topology with the diff data.
The resulting topology leave be written to diff_merged_topology dictionary.
Deleted node and link objects will be extended with the is_dead assign. For a better visual experience, deleted node icons will be customized (Incoming UI changes for that will be discussed below).
New node and link objects will glucinium extended with the is_new property.
Let's code:
def get_topology_diff(cached, current): """ Topology diff analyzer and source. Accepts two valid topology dicts every bit an stimulation. Returns: - dict with added and deleted nodes, - dict with added and deleted golf links, - dict with merged input topologies with extended attributes for topology changes visualization """ diff_nodes = {'added': [], 'deleted': []} diff_links = {'added': [], 'deleted': []} diff_merged_topology = {'nodes': [], 'golf links': []} # Parse links from topology dicts into the following format: # (topology_link_obj, (source_hostnme, source_port), (dest_hostname, dest_port)) 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['golf links']] # Parse nodes from regional anatomy dicts into the following format: # (topology_node_obj, (hostname,)) # Some additive values might glucinium added for comparison later on to the tuple above. cached_nodes = [(x, (x['name'],)) for x in cached['nodes']] nodes = [(x, (x['name'],)) for x in current['nodes']] # Research for deleted and added hostnames. 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['epithet']] = node_id raw_data['is_new'] = 'no more' raw_data['is_dead'] = 'nobelium' diff_merged_topology['nodes'].append(raw_data) node_id += 1 continue diff_nodes['added'].append(knob) raw_data['id'] = node_id host_id_map[raw_data['name']] = node_id raw_data['is_new'] = 'yes' raw_data['is_dead'] = 'atomic number 102' diff_merged_topology['nodes'].hang o(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'] = 'atomic number 102' raw_data['is_dead'] = 'yes' raw_data['icon'] = 'dead_node' diff_merged_topology['nodes'].append(raw_data) node_id += 1 # Seek for deleted and added interconnections. # Interface change on some side is considered A # ace interconnection deletion and one interconnection insertion. # Check for permutations as easily: # ((h1, Gi1), (h2, Gi2)) and ((h2, Gi2), (h1, Gi1)) are equal. link_id = 0 for raw_data, link in links: src, dst = yoke 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'].tack((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['poin'] = 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 = nexus if not (src, dst) in [x[1] for x in links] and not (dst, src) in [x[1] for x in golf 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'].add(raw_data) return diff_nodes, diff_links, diff_merged_topology get_topology_diff implements comparison of two arbitrary topology dictionaries of a valid format
This allows United States of America to implement a network topology cache versioning in the prospective.
Let's likewise implement a soothe diff photographic print serve:
def print_diff(diff_result): """ Formatted get_topology_diff result console print function. """ 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('No topology changes since last run.') return impress('Topology changes have been discovered:') if diff_nodes['added']: print('') print('^^^^^^^^^^^^^^^^^^^^') print('New Network Devices:') print('vvvvvvvvvvvvvvvvvvvv') for node in diff_nodes['added']: photographic print(f'Hostname: {node[0]}') if diff_nodes['deleted']: print('') mark('^^^^^^^^^^^^^^^^^^^^^^^^') impress('Deleted Network Devices:') mark('vvvvvvvvvvvvvvvvvvvvvvvv') for node in diff_nodes['deleted']: print(f'Hostname: {node[0]}') if diff_links['added']: publish('') print('^^^^^^^^^^^^^^^^^^^^^^') print('New Interconnections:') print('vvvvvvvvvvvvvvvvvvvvvv') for src, dst in diff_links['added']: print(f'From {src[0]}({src[1]}) To {dst[0]}({dst[1]})') if diff_links['deleted']: print('') print('^^^^^^^^^^^^^^^^^^^^^^^^^') print('Deleted Interconnections:') print('vvvvvvvvvvvvvvvvvvvvvvvvv') for src, dst in diff_links['deleted']: mark(f'From {src[0]}({src[1]}) To {dst[0]}({dst[1]})') print('') Finally, let's sum the code pieces we have written above into a dedicated chief() function.
Hera are a fairly self-documenting code and my personal answer to a doubtfulness "why non Ansible":
def good_luck_have_fun(): """Main script logic""" get_host_data_result = nr.carry(get_host_data) GLOBAL_LLDP_DATA, GLOBAL_FACTS = normalize_result(get_host_data_result) TOPOLOGY_DETAILS = extract_lldp_details(GLOBAL_LLDP_DATA) TOPOLOGY_DETAILS.tack(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('Open main.HTML in a project root with your browser to view the topology') 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') if topology_is_changed: photographic print('Open diff_page.hypertext mark-up language in a project root to view the changes.') print("Optionally, open main.hypertext mark-up language and click 'Presentation diff' button") other: # write current topology to diff file out if the cache is missing write_topology_file(TOPOLOGY_DICT, dst='diff_topology.js') if __name__ == '__main__': good_luck_have_fun() Examination
To begin with, let's restrict the access to dist-rtr01 and run the script. Resulting topology:
Then let's recoup the access to dist-rtr02, restrain the access to edge-sw01, and execute the hand again.
The previous version becomes cached. The latest topology looks as follows:
Diff topology file diff_topology.js based along their comparison.
volt-ampere topologyData = { "golf links": [ { "I.D.": 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", "generator": 4, "srcDevice": "dist-rtr01.devnet.research laboratory", "srcIfName": "Gi4", "direct": 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" }, { "I.D.": 3, "is_dead": "no", "is_new": "yes", "source": 4, "srcDevice": "dist-rtr01.devnet.lab", "srcIfName": "Gi5", "direct": 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": "nub-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.research 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.science lab", "srcIfName": "Eth1/2", "target": 2, "tgtDevice": "dist-sw02.devnet.lab", "tgtIfName": "Eth1/2" }, { "ID": 11, "is_dead": "yes", "is_new": "zero", "source": 7, "srcDevice": "border-sw01.devnet.research laboratory", "srcIfName": "Gi0/2", "target": 5, "tgtDevice": "core-rtr01.devnet.lab", "tgtIfName": "Gi0/0/0/1" }, { "I.D.": 12, "is_dead": "yes", "is_new": "No", "source": 7, "srcDevice": "march-sw01.devnet.lab", "srcIfName": "Gi0/3", "target": 3, "tgtDevice": "kernel-rtr02.devnet.lab", "tgtIfName": "Gi0/0/0/1" } ], "nodes": [ { "image": "router", "id": 0, "is_dead": "no more", "is_new": "no more", "model": "CSR1000V", "name": "dist-rtr02.devnet.lab", "serial_number": "9YZKNQKQ566", }, { "icon": "switch", "id": 1, "is_dead": "none", "is_new": "no", "model": "Nexus9000 9000v Chassis", "discover": "dist-sw01.devnet.lab", "serial_number": "9MZLNM0ZC9Z", }, { "icon": "change over", "id": 2, "is_dead": "no", "is_new": "no more", "model": "Nexus9000 9000v Chassis", "name": "dist-sw02.devnet.lab", "serial_number": "93LCGCRUJA5", }, { "icon": "router", "id": 3, "is_dead": "no", "is_new": "no", "theoretical account": "n/a", "describ": "pith-rtr02.devnet.lab", "serial_number": "n/a", }, { "icon": "router", "id": 4, "is_dead": "no", "is_new": "yes", "simulate": "CSR1000V", "describ": "dist-rtr01.devnet.research laboratory", "serial_number": "9S78ZRF2V2B", }, { "icon": "router", "id": 5, "is_dead": "no", "is_new": "no more", "example": "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.information", "serial_number": "9LGWPM8GTV6", }, { "icon": "dead_node", "id": 7, "is_dead": "yes", "is_new": "no more", "good example": "IOSv", "cite": "abut-sw01.devnet.laboratory", "serial_number": "927A4RELIGI", } ] }; A console production on the last run:
$ python3.7 generate_topology.py Open main.HTML in a jut root with your web browser to view the topology Topology changes sustain been determined: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Recent network devices: vvvvvvvvvvvvvvvvvvvvvvvvvvvvv Hostname: dist-rtr01.devnet.lab ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Deleted devices: vvvvvvvvvvvvvvvvvvvvvvvvvvvvv Hostname: edge-sw01.devnet.lab ^^^^^^^^^^^^^^^^^^^^^ Unaccustomed interconnections: vvvvvvvvvvvvvvvvvvvvv From dist-rtr01.devnet.lab(Gi3) To core-rtr02.devnet.lab(Gi0/0/0/2) From dist-rtr01.devnet.laboratory(Gi4) To dist-sw01.devnet.lab(Eth1/3) From dist-rtr01.devnet.lab(Gi6) To dist-rtr02.devnet.lab(Gi6) From dist-rtr01.devnet.lab(Gi5) To dist-sw02.devnet.research laboratory(Eth1/3) From dist-rtr01.devnet.laboratory(Gi2) To nitty-gritty-rtr01.devnet.lab(Gi0/0/0/2) ^^^^^^^^^^^^^^^^^^^^^^^^^ Deleted interconnections: vvvvvvvvvvvvvvvvvvvvvvvvv From edge-sw01.devnet.lab(Gi0/2) To core-rtr01.devnet.lab(Gi0/0/0/1) From edge-sw01.devnet.research laboratory(Gi0/3) To core-rtr02.devnet.lab(Gi0/0/0/1) Open diff_page.html to regar the changes. Optionally, exposed main.html and click the 'Exhibit diff' button Everything looks correct. The output matches the changes.
To visualize the diff topology properly, we volition make or s adjustments in next_app.js below.
Enhancing Close UI application
Just about of the improvements down the stairs were ready-made supported the examples in the Next UI documentation and tutorials.
Adding interface labels
Ready to add the interface labels, let's carry the standard nx.graphic.Topology.Link category:
nx.define('CustomLinkClass', nx.graphic.Topographic anatomy.Link, { properties: { sourcelabel: cypher, targetlabel: null }, view: function(persuasion) { view.content.push({ name: 'reference', type: 'nx.graphic.Text', props: { 'class': 'sourcelabel', 'conjunction-baseline': 'text-later on-edge', 'schoolbook-anchor': 'come out' } }, { mention: 'place', type: 'nx.graphic.Textual matter', props: { 'class': 'targetlabel', 'alignment-baseline': 'text edition-after-butt on', 'text-anchor': 'end' } }); return view; }, methods: { update: function() { this.inherited(); var el, point; var telephone line = this.line(); var angle = line.lean(); var stageScale = this.stageScale(); line = line.pad(18 * stageScale, 18 * stageScale); if (this.sourcelabel()) { el = this.view('source'); point = line.start; overhead railway.set('x', point.x); el.set('y', compass point.y); altitude.ready('text', this.sourcelabel()); el.set('transform', 'rotate(' + angle + ' ' + point.x + ',' + bespeak.y + ')'); el.setStyle('font-size of it', 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()); elevated.set('transform', 'rotate(' + lean against + ' ' + point.x + ',' + point.y + ')'); altitude.setStyle('font-size', 12 * stageScale); } } } }); A customized link class can now be listed in the topo topology physical object properties.
Let's also highlight the links on the diff topology. New links will cost green, deleted links will be red and dashed.
linkConfig: { linkType: 'curve', sourcelabel: 'theoretical account.srcIfName', targetlabel: 'model.tgtIfName', style: function(model) { if (model._data.is_dead === 'yes') { // Deleted links curb 'is_dead' attribute set to 'yes'. // Make them dotted. counte { 'virgule-dasharray': '5' } } }, color: office(mould) { if (model._data.is_dead === 'yes') { // Deleted golf links contain 'is_dead' attribute set to 'yes' // Make them colored. return '#E40039' } if (model._data.is_new === 'yes') { // Unexampled golf links contain 'is_new' attribute set to 'yes' // Name them viridity. return '#148D09' } }, }, // Use elongated link class version with interface labels enabled linkInstanceClass: 'CustomLinkClass' Adding custom node icons
Incoming UI already contains a broad set of default icons for network devices.
However, you are free to add custom-built icons based on your requirements. For deleted nodes, we motive something special.
To add a new icon, you should put an image into a directory accessible aside Incoming UI and initialize information technology in topo object.
// the image is saved to ./img/dead_node.png topo.registerIcon("dead_node", "img/dead_node.png", 49, 49); Getting diff visualization through
We have actually done everything we need. Have's open diff_page.html and see how the changes we have done before look on like:
A topology view speaks for itself, doesn't it?
Updating a thickening tooltip
By default, a guest tooltip contains excessive information suchlike internal node Idaho and coordinates.
In NeXt UI, it is possible to custom-make it for better legibility and usability.
Let's let in the pursuing information for now:
- Device hostname.
Let's also micturate hostname a customizable link to an arbitrary resourcefulness. It power be a device page in Netbox or a monitoring system.
The link template will be stored in the dcimDeviceLink variable.
It can be added during the topology contemporaries work on. In the case of missing value, the hostname will just be just a simple text. - Gimmick IP-Address, asynchronous number, and model.
In order to implement this, let's extend a standard nx.ui.Part class and build a simle Hypertext markup language form inside of it:
nx.define('CustomNodeTooltip', nx.ui.Portion, { properties: { lymph node: {}, topology: {} }, take i: { content: [{ tag: 'div', content: [{ tag: 'h5', content: [{ tag: 'a', content: '{#guest.model.list}', props: {"href": "{#node.example.dcimDeviceLink}"} }], props: { "style": "bound-bottom: dotted 1px; baptistry-sized:90%; word-wrap:normal; color:#003688" } }, { tag: 'p', content: [ { tag: 'label', content: 'IP: ', }, { tag: 'label', substance: '{#node.posture.primaryIP}', } ], props: { "style": "font-size up:80%;" } },{ tag: 'p', placid: [ { track: 'label', content: 'Simulate: ', }, { rag: 'label', content: '{#client.mould.model}', } ], props: { "style": "font-size:80%;" } }, { tag: 'p', content: [{ tag: 'label', content: 'S/N: ', }, { tag: 'label', content: '{#node.pose.serial_number}', }], props: { "style": "font-size:80%; cushioning:0" } }, ], props: { "style": "width: 150px;" } }] } }); nx.define('Tooltip.Node', nx.ui.Component, { view: function(view){ purview.content.push({ }); return view; }, methods: { confiscate: function(args) { this.inherited(args); this.model(); } } }); Now the custom class interpretation can exist listed in topo topology object properties:
tooltipManagerConfig: { nodeTooltipContentClass: 'CustomNodeTooltip' }, This is what we visualize happening click on the node afterward that:
Customizing node layout
American Samoa discussed before, the default Next UI data CPU is 'coerce'. IT relies on the best effort algorithmic program spreading the Nodes thusly they would be as aloof from each early as possible.
This logic produces a proper layout even for complex hierarchical network topologies simply the topology layers may non personify destined as desired. For sure, you lavatory drag them manually later o. Still, this is not our way.
Fortunately, in that respect are some made-up-in tools to sour with layers in Next UI.
Let's use a new quantitative layerSortPreference property inside nodes in our Next UI application.
The logic shaping this esteem can comprise implemented outside the visualization application along the topology object multiplication stage in this case. Next UI would just order the layers in a mode we separate it to. This is a more scalable approach.
Let's tote up some functions to be fit to substitution between layouts:
var currentLayout = 'auto' horizontal = function() { if (currentLayout === 'horizontal') { return; }; currentLayout = 'naiant'; volt-ampere layout = topo.getLayout('hierarchicalLayout'); layout.direction('horizontal'); layout.levelBy(function(node, model) { return model.start('layerSortPreference'); }); topo.activateLayout('hierarchicalLayout'); }; vertical = function() { if (currentLayout === 'erectile') { return; }; currentLayout = 'vertical'; var layout = topo.getLayout('hierarchicalLayout'); layout.direction('vertical'); layout.levelBy(function(node, model) { return mannikin.get('layerSortPreference'); }); topo.activateLayout('hierarchicalLayout'); }; Map these social occasion to button elements connected main.html and diff_page.html:
<button onclick='horizontal()'>Horizontal layout</button> <push onclick="vertical()">Straight layout</button> Let's improve the generate_topology.py script and add some additional attributes to Nornir hosts file to implement an automatic node hierarchy calculation.
The script will define an organized list of anthropomorphic-friendly layer names and make a rebirth into numeric values:
# Topology layers would be sorted # in the same descending order # as in the tuple below NX_LAYER_SORT_ORDER = ( 'undefinable', 'outside', 'boundary-switch', 'edge-router', 'marrow-router', 'core-switch', 'distribution-router', 'statistical distribution-switch', 'flick', 'rachi', 'access-exchange' ) def get_node_layer_sort_preference(device_role): """Layer priority selection function Layer sorting preference is designed A a numeric value. This purpose identifies it by NX_LAYER_SORT_ORDER object position by default. With numeric values, the logic may atomic number 4 improved without changes on the NeXt app go with. 0(null) causes an indefinable layer position in the NeXt UI. Valid indexes originate in with 1. """ for i, role in enumerate(NX_LAYER_SORT_ORDER, start=1): if device_role == role: return i return 1 A numeric stratum sort society for the layer will be outlined by its relational position in NX_LAYER_SORT_ORDER.
Important: NeXt UI interprets 0(goose egg) as undefined. Valid layer indexes showtime with 1.
The device layer will be based on its office property inside the Nornir Hosts Inventory file.
information field allows us to specify a list of attributes with arbitrary names:
dist-rtr01: hostname: 10.10.20.175 political platform: ios groups: - devnet-cml-lab information: role: statistical distribution-router Any attribute inside host information can and then atomic number 4 known as in Python as a dict key as follows:
nr.inventory.hosts[device].get('some_attribute_name') To reflect the changes, lease's update our Python encode. A new nr_role knob property will be appended along with others into global_facts in spite of appearanc the normalize_result function:
# Full function is omitted Here for brevity global_facts[device_fqdn]['nr_role'] = nr.inventory.hosts[device].fix('role', 'undefined') Past we should read this attribute during node object generation inside the generate_topology_json function:
# Entire function is omitted here for brevity device_role = facts[host].get('nr_role', 'undefined') topology_dict['nodes'].supply({ 'id': host_id, 'name': host, 'primaryIP': device_ip, 'pattern': device_model, 'serial_number': device_serial, 'layerSortPreference': get_node_layer_sort_preference( device_role ), 'ikon': get_icon_type( lldp_capabilities_dict.sire(host, ''), device_model ) }) And now we can control the chaos align the layers horizontally and vertically along button flick. This is how information technology looks like:
Resulting project construction
The final see structure looks as follows:
$ corner . -L 2 . ├── LICENSE ├── README.Dr. ├── diff_page.html ├── diff_topology.js ├── generate_topology.py ├── img │ └── dead_node.png ├── stock │ ├── groups.yml │ └── hosts_devnet_sb_cml.yml ├── main.html ├── next_app.js ├── next_sources │ ├── css │ ├── doctor │ ├── 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 Ratiocination
First of every, give thanks you for meter reading. I desire you enjoyed it.
In this article, I tried to regurgitate and document the solution creation stages and my considerations behind them.
A full source inscribe is available on my GitHub page.
The solution took first home connected the Marathon based on attendees' and organizers' votes. And equally important, it has a potentiality for reuse and scalability(spoiler: I highly-developed a Netbox plugin reusing the core code from this project).
How do you like the solution? What could be improved? How would you wor this task?
Delight palpate unloosen to share your ain experience and thoughts.
Free Cisco Network Drawing Tool
Source: https://habr.com/en/post/534716/
Post a Comment for "Free Cisco Network Drawing Tool"