<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://idebugall.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://idebugall.github.io/" rel="alternate" type="text/html" /><updated>2025-05-26T15:46:10+00:00</updated><id>https://idebugall.github.io/feed.xml</id><title type="html">iDebugAll - Igor Korotchenkov’s Blog</title><subtitle>NetDevOps, Programming, and Common Sense.</subtitle><entry><title type="html">NetBox as Voice and UC Source of Truth</title><link href="https://idebugall.github.io/phonebox-init/" rel="alternate" type="text/html" title="NetBox as Voice and UC Source of Truth" /><published>2021-03-06T00:00:00+00:00</published><updated>2021-03-06T00:00:00+00:00</updated><id>https://idebugall.github.io/phonebox-init</id><content type="html" xml:base="https://idebugall.github.io/phonebox-init/"><![CDATA[<p>Have you ever been struggling with an enterprise Voice and Unified Communications infrastructure 
documentation?</p>

<ul>
  <li>What phone number is that? Where it comes from?</li>
  <li>Is this SIP-trunk relevant?</li>
  <li>Which of these Excel files contains the information I need?</li>
  <li>Do we have a spare phone number for a new service?</li>
  <li>Phone_numbers_latest_201907(3).xlsx?!</li>
</ul>

<p>Sounds painfully familiar? I have got something that might help.</p>

<cut />

<p>TLDR: I developed a <a href="https://github.com/iDebugAll/phonebox_plugin">new plugin</a> for <a href="https://github.com/netbox-community/netbox">NetBox</a> to manage phone numbers and more. Using NetBox as Voice and UC Source of Truth might be a good idea.</p>

<h2 id="how-and-what-enterprises-document">How and What Enterprises Document</h2>

<p>I have seen many examples of how the documentation can be organized since 2011 when I started my career. I’ve been managing and consulting on Voice and UC deployments serving thousands of users and consisting of hundreds of voice devices and circuits in total. However, regardless of the region and size, they all shared one thing in common: the documentation related to Voice and UC relied on Microsoft Office and PDF files stored in a more or less structured fashion.</p>

<p>It is a good question, how representative this sample is. Based on discussions with other engineers, this is quite typical. Dedicated domain-specific documenting solutions for Voice and UC are not that common in the enterprise sector. “What is the problem with that?” you may ask. Let’s try to figure this out.</p>

<p>To begin with, I would break the pieces of the information we are usually interested in and keeping track of as follows:</p>

<ol>
  <li><strong>Solution design documents and implementation summaries.</strong>
<em>How it was designed and expected to work</em>.
You can typically find such files in most organizations.</li>
  <li><strong>Network and domain-specific diagrams.</strong>
<em>How it looks visually</em>.
This might be a part of #1.</li>
  <li><strong>Inventory information.</strong>
<em>What physical and virtual boxes we have</em>: Voice and UC equipment, virtual machines, PRI and DSP modules, etc. Related models, part IDs, serial numbers typically fall into this category.</li>
  <li><strong>Cabling and device interconnection details.</strong>
<em>Cable paths across patch panels and between ports</em>.
Those who spent hours with a <a href="https://www.amazon.com/Generator-Accuracy-Inductive-Amplifier-Adjustable/dp/B08DV1N2Q9/">tone generator and wire tracker</a> trying to identify the actual cross-connections from a traditional PBX will likely sigh at this point. <em>*Sighs*</em>. In VoIP environments, this information is still useful to know.</li>
  <li><strong>IP addressing.</strong>
<em>How we assign IP-addresses to our devices</em>.</li>
  <li><strong>Voice Circuits.</strong>
<em>What voice interconnections we have</em>: SIP-trunks, PRIs, CO lines.
It is necessary to know what their requirements are and how many lines they offer.</li>
  <li><strong>Phone numbers.</strong>
<em>What PSTN pools and internal extensions we have</em>.</li>
  <li><strong>Call routing.</strong>
<em>How we route phone numbers and patterns</em>.</li>
  <li><strong>Configuration details and templates.</strong>
<em>What configuration elements translate #1 into a working infrastructure</em>.
The actual configurations on the devices and VMs are often the only source of such information.</li>
  <li><strong>Management access details.</strong>
<em>How do we get access to our infrastructure elements</em>.</li>
  <li><strong>External contracts and agreements.</strong>
<em>What external services we have and what we billed for</em>.
Searching phone numbers through PDF scans of the service provider agreements is adventurous. <em>*Sighs again*.</em></li>
</ol>

<p>The list demonstrates some general categories. It might change from one organization to another.
There is no single format convention nor standard templates across different organizations.
Furthermore, some additional challenges arise with regular text files and spreadsheets:</p>

<ol>
  <li>There are no clear relations between each piece of the information. Especially cross-document.</li>
  <li>It is hard to search and index the information across distributed files.</li>
  <li>It heavily relies on your knowledge of each file’s location and purpose.</li>
  <li>Some information may be duplicated across the documents.</li>
  <li>It is hard to validate the information and compare it with the actual infrastructure state.</li>
  <li>Reporting requires extra effort and manual data transformation and analysis.</li>
  <li>A whole picture may be missing. UC and Network teams may operate independently. The larger organization is, the higher chances are. Cross-functional tasks like setting up and maintaining Voice QoS may become much harder in such cases.</li>
</ol>

<p>The information we store in our documentation should be up-to-date and consistent to help us operate our infrastructures reliably and efficiently. <em>Some</em> documentation is usually better than no documentation at all. However, blindly trusting the documentation may lead to even worse scenarios. The need to double-check the actual state every time eventually pushes us to avoid opening to the documentation and forget to update it afterwards. As long as the configuration changes can be applied unreflected in the documentation, sooner or later, they will.</p>

<p>Text files and spreadsheets are not necessarily a poor choice. General design documents are more self-sufficient and tend to stay valid in a long-term perspective. Regular file formats are typically acceptable and suitable for them. Frequently updated operational documentation, in contrast, requires different approaches to overcome the limitations listed above.</p>

<h2 id="uc-infrastructure-as-code-and-uc-source-of-truth">UC Infrastructure-as-Code and UC Source-of-Truth</h2>

<p>One of the solutions the industry has come to is Infrastructure-as-Code (IaC) paired with Single-Source of-Truth. It redefines the idea of how we treat our infrastructures and perform changes:</p>

<ul>
  <li>An infrastructure representation moves toward machine-readable scripted or declarative configurations. Using this approach, you can apply the advantages and best practices of software development, testing, and DevOps.</li>
  <li>Single Source of Truth (or simply Source-of-Truth, SoT) is an idea of identifying a single place for each piece of the information about our infrastructure where it can be found and must be managed. It does not mean all the data has to be stored in a single place. It also does not assume the individual pieces of data can not exist in multiple copies anymore. The key requirement is that the primary source for each piece of data must be unique.</li>
  <li>Changes are applied to the infrastructure components by the teams indirectly. You change the data in your Souce-of-Truth first. Then it gets validated, applied to the IaC model, tested, and eventually delivered to the target devices and VMs by the Automation stack of your choice.</li>
</ul>

<p>Souce-of-Truth contains a desirable state of your infrastructure. The purpose of the automation stack is to bring the actual configurations in compliance with the Source-of-Truth based on Infrastructure object models you store as code and scripts. It eliminates the problem of inconsistent documentation as now you change your infrastructure by updating the documentation.</p>

<p>It is vital to notice that moving from the traditional workflows to the Source-of-Trust and Infrastructure-as-Code does not has to be done in a single breaking step. It is possible to migrate the processes and procedures gradually. Rebuilding the documentation framework might be a good starting point.</p>

<p>Well, with <a href="https://xkcd.com/505/">enough time and perseverance</a>, you can implement the ultimately complex scenarios inside MS Excel as its formulas <a href="https://www.techrepublic.com/article/microsoft-turning-excel-into-a-turing-complete-programming-language/">are Turing-complete</a>. Supporting and maintaining such a solution might be a nightmare. An ability to integrate it with the outer systems will also be limited. That leads us to the need for specialized tools.</p>

<p>These tools and concepts are already widely used by DevOps engineers.<br />
Being applied to Networks, NetDevOps has greatly evolved during the past few years.<br />
I believe all these practices are applicable to the core UC domain as well. Not to mention that UC components might even share the platforms with regular Network functions. Initial provisioning steps for an SBC and a Router or a Switch are very similar. Automating BGP peering is not too far from automating SIP trunking.<br />
Many best practices and tools from the NetDevOps are transferable to the UC domain. One of such tools is NetBox.</p>

<h2 id="why-netbox">Why NetBox</h2>

<p>And an obvious question is “What is NetBox?” A comprehensive answer from its developers:</p>

<blockquote>
  <p>NetBox is an open source web application designed to help manage and document computer networks. Initially conceived by the network engineering team at DigitalOcean, NetBox was developed specifically to address the needs of network and infrastructure engineers. It encompasses the following aspects of network management:</p>

  <ul>
    <li><strong>IP address management (IPAM)</strong> - IP networks and addresses, VRFs, and VLANs</li>
    <li><strong>Equipment racks</strong> - Organized by group and site</li>
    <li><strong>Devices</strong> - Types of devices and where they are installed</li>
    <li><strong>Connections</strong> - Network, console, and power connections among devices</li>
    <li><strong>Virtualization</strong> - Virtual machines and clusters</li>
    <li><strong>Data circuits</strong> - Long-haul communications circuits and providers</li>
    <li><strong>Secrets</strong> - Encrypted storage of sensitive credentials</li>
  </ul>
</blockquote>

<p>NetBox is designed as the Network Source-of-Truth. Everything in NetBox is an object, and <em>almost</em> everything is accessible via API that provides great opportunities for NetBox integration with external systems. There are a relational database and data model behind NetBox objects. NetBox is well <a href="https://netbox.readthedocs.io/en/stable/">documented</a> and has great <a href="https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ">community</a> around it. Not surprisingly, the popularity of NetBox keeps growing worldwide. Chances are, your Network team is already aware of NetBox.</p>

<p>You might have noticed that the list above already covers several categories from the list of Voice and UC documentation. PBXs, SBCs, Gateways, MCUs, and many other Voice and UC boxes are <strong>Devices</strong>. You can usually find them installed into the <strong>Equipment Racks</strong> below the ToR switches. They are interconnected with <strong>Connections</strong> and may terminate some <strong>Data Circuits</strong> carrying Voice signaling and media from the Telephony Service <strong>Providers</strong>. Some of Voice and UC functions may be running as <strong>Virtual Machines</strong>. And all (unless you are using electromechanical PBXs) Voice and UC infrastructure components use <strong>IP-addresses</strong>.</p>

<p>But some critical points like <strong>Phone Numbers</strong>, <strong><em>Voice</em></strong> <strong>Circuits</strong>, and <strong>Call Routing</strong> are missing. Fortunately, NetBox provides extremely powerful extensibility feature called <a href="https://netbox.readthedocs.io/en/stable/plugins/">Plugins</a>. It allows you to extend the core NetBox data schema and embed new features. Combined with native NetBox data models, such a plugin would provide you with a full set of necessary abstractions to describe your Voice and UC deployment.</p>

<p>Some of the primary benefits of this approach:</p>
<ul>
  <li>It simplifies the modeling of the cross-domain relations and dependencies. UC is highly dependant on the underlying Network infrastructure.</li>
  <li>Unified documentation and Source of Truth framework for Network and <em>Unified</em> Communications provides you with full visibility of your infrastructure.</li>
  <li>Common tools may simplify cross-team communications.</li>
  <li>Reporting becomes more agile and powerful.</li>
</ul>

<p>So I introduce the <a href="https://github.com/iDebugAll/phonebox_plugin">PhoneBox</a> plugin for NetBox.</p>

<h2 id="phonebox-plugin">PhoneBox Plugin</h2>

<p>This plugin is intended to bridge the gap between general NetBox network data models and those of Voice and UC world.
I started development recently and already implemented <strong>Phone Numbers</strong>. It provides some instant value and allows you to manage PSTN DIDs and internal extensions using NetBox. Some <a href="https://github.com/netbox-community/netbox/issues/1893">feature requests</a> from the NetBox community to add support for such abstractions were already there.</p>

<p>Each Phone Number consists of the following attributes:</p>

<ul>
  <li>Number – An individual phone number.</li>
  <li>Tenant – A mandatory relation to Netbox <em>Tenant</em> object. It allows for number plan partitioning.</li>
  <li>Description – An optional text description for the number.</li>
  <li>Provider – An optional relation to NetBox native <em>Provider</em> object. It identifies the provider of the number.</li>
  <li>Region – An optional relation to NetBox native <em>Region</em>. It represents the geographic location of the number.</li>
  <li>Forward_To – an optional relation to another Number object. It represents a Call Forwarding scenario.</li>
  <li>Tags – An optional relation to NetBox native tag objects.</li>
</ul>

<p>The plugin implements all Create, Read, Update, Delete (CRUD) operations for the Phone Numbers via NetBox web interface and REST API. 
It also supports bulk import from CSV for the Phone Numbers to simplify the migration of your data.</p>

<p>I am planning to add more Voice and UC abstractions to the plugin in the future.
The complexity of selecting proper data models that fit into arbitrary infrastructure is hard to underestimate. I am going to write a separate post about it.</p>

<p>Please feel free to leave the comments below or to reach me on social media. Your feedback matters.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Have you ever been struggling with an enterprise Voice and Unified Communications infrastructure documentation? What phone number is that? Where it comes from? Is this SIP-trunk relevant? Which of these Excel files contains the information I need? Do we have a spare phone number for a new service? Phone_numbers_latest_201907(3).xlsx?! Sounds painfully familiar? I have got something that might help.]]></summary></entry><entry><title type="html">Implementing Offline traceroute Tool Using Python</title><link href="https://idebugall.github.io/traceroute-by-rt/" rel="alternate" type="text/html" title="Implementing Offline traceroute Tool Using Python" /><published>2021-01-30T00:00:00+00:00</published><updated>2021-01-30T00:00:00+00:00</updated><id>https://idebugall.github.io/traceroute-by-rt</id><content type="html" xml:base="https://idebugall.github.io/traceroute-by-rt/"><![CDATA[<p>This post demonstrates an interesting use-case of radix tries usage for Longest Prefix Match.</p>

<p>The post was born from a question asked by an IT forum member. The summary of the question looked as follows:</p>

<ul>
  <li>There is a set of text files containing routing tables collected from various network devices.</li>
  <li>Each file represents one device.</li>
  <li>Device platforms and routing table formats may vary.</li>
  <li>It is required to analyze a routing path from any device to an arbitrary subnet or host on-demand.</li>
  <li>Resulting output should contain a list of routing table entries that are used for the routing to the given destination on each hop.</li>
</ul>

<p>The one who asked a question worked as a TAC engineer. It is often that they collect or receive from the customers some text ‘snapshots’ of the network state for further offline analysis while troubleshooting the issues. Some automation could really save a lot of time.</p>

<p>I found this task interesting and also applicable to my own needs, so I decided to write a Proof-of-Concept implementation in Python 3 for Cisco IOS, IOS-XE, and ASA routing table format.</p>

<p>In this article, I’ll try to reconstruct the resulting script development process and my considerations behind each step.</p>

<cut />

<h2 id="disclaimer">Disclaimer</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>This is a translation of my original article in Russian I posted in June 2018.
If you are willing to help to improve the translation, please DM me.
All listed code is published under MIT license and does not provide guarantees of 
any kind.

The solution is written in Python 3.7.
An understanding of the programming and networking basics is desirable for reading.

If you found a typo, please use Ctrl+Enter or ⌘+Enter to send it to the author.
</code></pre></div></div>

<h1 id="task-decomposition-and-requirements-analysis">Task Decomposition and Requirements Analysis</h1>

<p>Considering the initial task summary, I would split it into two main parts:</p>
<ul>
  <li>Extracting the routing tables from the text files into some representation in Python data structures.</li>
  <li>Analyzing routing paths based on that pre-processed routing data.</li>
</ul>

<p>Such logic separation will allow us to import the routing data from different sources (e.g. APIs or SNPM) and not limiting the potential scope to text file input.</p>

<p>To improve route lookup performance, it is necessary to initialize the files just once on script startup. 
The solution will support Cisco IOS, IOS-XE, and ASA routing table output format for IPv4. Extensibility logic applies here as well.</p>

<p>As we know, a route selection during routing table lookup relies on a Longest Prefix Match (LPM) logic. Unlike with Access Control lists, it is not enough to pick the first match. We have to find the most specific one.</p>

<p>Fortunately, there are fast algorithms and approaches for LPM calculation. One of them is building a so-called prefix tree (prefix trees may also be referenced to as Subnet Tries, Patricia Tries, or Radix Trees in the general case) based on a routing table.</p>

<p>In prefix trees, the lookup speed does not depend on the tree size (the number of prefixes), it only depends on a tree depth (maximum prefix length) which is a maximum of 32 for IPv4 (programmers would say that the search time complexity is O(k), where <em>k</em> is the maximum prefix length). In other words, route lookup using prefix trees will work with the same average speed on routing tables containing 500 and 500,000 routes.</p>

<p>Initial tree building time does linearly depend on a number of prefixes and their length (programmers would say that the build time complexity is O(n*k) where <em>n</em> is the number of prefixes and <em>k</em> is the maximum prefix length), but we do this just once. Any subsequent search request will work super fast.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A detailed explanation of this algorithm deserves a dedicated article.
Please let me know if you are interested.
</code></pre></div></div>
<p>As we are not limited in usage of an external dependencies, we can use some existing Python library implementing this. Some of them are:</p>

<ul>
  <li><a href="https://pypi.org/project/pysubnettree/">SubnetTree</a>. I wrote my original solution based on this library. It works really fast. But its internal code is written in C++ which causes some compatibility issues and dependency hell across operating systems.</li>
  <li><a href="https://github.com/jsommers/pytricia">PyTricia</a>. This library looks best in terms of performance and compatibility in early 2021. It is written in C, so it should work more seamlessly in different OSs. So I migrated my solution to this library. Thanks to the selected code design, it was a matter of a few changed lines of code.</li>
</ul>

<p>We should also keep in mind that the analyzed network segment may contain routing loops. They should be detected. It should not break the script execution.<br />
The routers may also have no route to the destination. That’s another point to note.</p>

<p>Routing tables for <a href="https://www.cisco.com/c/en/us/td/docs/solutions/Enterprise/Network_Virtualization/PathIsol.html#wp80043">VRF</a>s, if present, should be saved into dedicated text files as each VRF instance represents a separate logical device from the routing and topology perspective.</p>

<p>Hardware performance limitations for script execution, the potential size of the routing tables and the network segments were not in a list of initial requirements. However, let’s take them into account. 
Most modern network platforms support over 1M routing entries on average. IPv4 BGP Full View size is around 814,000 prefixes as of January 2021.</p>

<p>My rough estimations and tests showed that in-memory processing of each 1M prefixes requires ~500MB of RAM for this scenario. Even a mid-level laptop with 8GB RAM should allow you to process a topology consisting of 17-18 routers with Full View on each of them (~12-13M prefixes in total). I believe this is enough for most of the cases. For larger network segments, the analysis can either be split into smaller scopes or moved into an external out-of-memory database.<br />
<del>640 kB ought to be enough for anybody.</del> Let’s stick on an in-memory processing option.</p>

<h2 id="parsing-source-files-and-selecting-data-structures">Parsing Source Files and Selecting Data Structures</h2>

<p>Let’s store all our text files with routing tables in a separate variable-defined sub-directory:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">RT_DIRECTORY</span> <span class="o">=</span> <span class="s">"./routing_tables"</span>
</code></pre></div></div>

<p>Here is a reference of IOS and IOS-XE routing table output format:</p>

<p><em>show ip route</em></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>S* 0.0.0.0/0 [1/0] via 10.220.88.1
10.0.0.0/8 is variably subnetted, 2 subnets, 2 masks
C 10.220.88.0/24 is directly connected, FastEthernet4
L 10.220.88.20/32 is directly connected, FastEthernet4
     1.0.0.0/32 is subnetted, 1 subnets
S       1.1.1.1 [1/0] via 212.0.0.1
                [1/0] via 192.168.0.1
D EX     10.1.198.0/24 [170/1683712] via 172.16.209.47, 1w2d, Vlan910
                       [170/1683712] via 172.16.60.33, 1w2d, Vlan60
                       [170/1683712] via 10.25.20.132, 1w2d, Vlan220
                       [170/1683712] via 10.25.20.9, 1w2d, Vlan20
     4.0.0.0/16 is subnetted, 1 subnets
O E2    4.4.0.0 [110/20] via 194.0.0.2, 00:02:00, FastEthernet0/0
     5.0.0.0/24 is subnetted, 1 subnets
D EX    5.5.5.0 [170/2297856] via 10.0.1.2, 00:12:01, Serial0/0
     6.0.0.0/16 is subnetted, 1 subnets
B       6.6.0.0 [200/0] via 195.0.0.1, 00:00:04
     172.16.0.0/26 is subnetted, 1 subnets
i L2    172.16.1.0 [115/10] via 10.0.1.2, Serial0/0
     172.20.0.0/32 is subnetted, 3 subnets
O       172.20.1.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0
O       172.20.3.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0
O       172.20.2.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0
     10.0.0.0/8 is variably subnetted, 5 subnets, 3 masks
C       10.0.1.0/24 is directly connected, Serial0/0
D       10.0.5.0/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0
D       10.0.5.64/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0
D       10.0.5.128/26 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0
D       10.0.5.192/27 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0
     192.168.0.0/32 is subnetted, 1 subnets
D       192.168.0.1 [90/2297856] via 10.0.1.2, 00:12:03, Serial0/0
O IA 195.0.0.0/24 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0
O E2 212.0.0.0/8 [110/20] via 194.0.0.2, 00:05:35, FastEthernet0/0
C    194.0.0.0/16 is directly connected, FastEthernet0/0
</code></pre></div></div>

<p><br /></p>

<p>Cisco ASA looks very similar. The difference is ASA displays full subnet masks instead of prefix lengths:</p>

<p><em>show route</em></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>S    10.1.1.0 255.255.255.0 [3/0] via 10.86.194.1, outside
C    10.86.194.0 255.255.254.0 is directly connected, outside
S*   0.0.0.0 0.0.0.0 [1/0] via 10.86.194.1, outside
</code></pre></div></div>

<p><br /></p>

<p>The examples show that, despite the multitude of options, all routing table entries have a predictable format. So they can be processed with regular expressions.<br />
There are two common groups based on route entry format: <em>Local+Connected</em> types and all the rest.</p>

<p>The existence of multi-line routes for multi-path routing cases makes them harder to extract. We can not use simple line iteration through the content of the files because of that. One of the workarounds is to iterate through regular expression matches covering multiple lines.<br /></p>

<p>Let’s write such regular expressions:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Local and Connected route strings matching.
</span><span class="n">REGEXP_ROUTE_LOCAL_CONNECTED</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span>
    <span class="sa">r</span><span class="s">'^(?P&lt;routeType&gt;[L|C])\s+'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'((?P&lt;ipaddress&gt;\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'\s?'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(?P&lt;maskOrPrefixLength&gt;(\/\d\d?)?'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'\ is\ directly\ connected\,\ '</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(?P&lt;interface&gt;\S+)'</span><span class="p">,</span>
    <span class="n">re</span><span class="p">.</span><span class="n">MULTILINE</span>
<span class="p">)</span>

<span class="c1"># Static and dynamic route strings matching.
</span><span class="n">REGEXP_ROUTE</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span>
    <span class="sa">r</span><span class="s">'^(\S\S?\*?\s?\S?\S?)'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'\s+'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'((?P&lt;subnet&gt;\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'\s?'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(?P&lt;maskOrPrefixLength&gt;(\/\d\d?)?'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'|(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)?))'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'\s*'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(?P&lt;viaPortion&gt;(?:\n?\s+(\[\d\d?\d?\/\d+\])\s+'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?)(.*)\n?)+)'</span><span class="p">,</span>
    <span class="n">re</span><span class="p">.</span><span class="n">MULTILINE</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Both regular expressions contain (<a href="https://docs.python.org/2/library/re.html">named groups</a>) to make them more readable and maintainable.
You can reference the named group value within the regular expression match by its key: <em>subnet</em>/<em>interface</em>/<em>maskOrPrefixLength</em> for the prefix info and <em>viaPortion</em>/<em>interface</em> for the route destination info in our case.</p>

<p>The regular expression covers subnet mask and prefix length representations at once. It can be extracted by <em>maskOrPrefixLength</em> key. For a further processing, let’s bring it to a common format of the prefix length as it is shorter:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">convert_netmask_to_prefix_length</span><span class="p">(</span><span class="n">mask_or_pref</span><span class="p">):</span>
    <span class="s">"""
    Gets subnet_mask (XXX.XXX.XXX.XXX) of /prefix_length (/XX).
    For subnet_mask, converts it to /prefix_length and returns the result.
    For /prefix_length, returns as is.
    For empty input, returns "" string.
    """</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">mask_or_pref</span><span class="p">:</span>
        <span class="k">return</span> <span class="s">""</span>
    <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s">"^\/\d\d?$"</span><span class="p">,</span> <span class="n">mask_or_pref</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">mask_or_pref</span>
    <span class="k">if</span> <span class="n">re</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s">"^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?$"</span><span class="p">,</span>
                <span class="n">mask_or_pref</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">(</span>
            <span class="s">"/"</span>
            <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">sum</span><span class="p">([</span><span class="nb">bin</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">)).</span><span class="n">count</span><span class="p">(</span><span class="s">"1"</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">mask_or_pref</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"."</span><span class="p">)]))</span>
        <span class="p">)</span>
    <span class="k">return</span> <span class="s">""</span>
</code></pre></div></div>

<p>Let’s also write a regular expression for next-hop extraction from the <em>viaPortion</em> group and a regular expression for IPv4 address format check in a file and user input:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Route string VIA portion matching.
</span><span class="n">REGEXP_VIA_PORTION</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span>
    <span class="sa">r</span><span class="s">'.*via\s+(\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?).*'</span>
<span class="p">)</span>
<span class="c1"># RegEx template string for IPv4 address matching.
</span><span class="n">REGEXP_IPv4_STR</span> <span class="o">=</span> <span class="p">(</span>
    <span class="sa">r</span><span class="s">'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.'</span>
    <span class="o">+</span> <span class="sa">r</span><span class="s">'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))'</span>
<span class="p">)</span>
<span class="c1"># IPv4 CIDR notation matching in user input.
</span><span class="n">REGEXP_INPUT_IPv4</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span><span class="sa">r</span><span class="s">"^"</span> <span class="o">+</span> <span class="n">REGEXP_IPv4_STR</span> <span class="o">+</span> <span class="sa">r</span><span class="s">"(\/\d\d?)?$"</span><span class="p">)</span>
</code></pre></div></div>

<p>Now let’s translate our network representation into Python data structures.
All the prefixes we extract from the routing tables will be used as prefix tree keys. Each prefix tree object will be inherited from the <em>PyTricia</em> module. Search result on a prefix tree will return a list of available next-hops and a full-text representation of the matched routing entry. Another list will store a list of local interfaces with their IP-addresses for each router.
Each router will be represented by a dictionary object containing all above.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example data structures
</span><span class="n">route_tree</span> <span class="o">=</span> <span class="n">pytricia</span><span class="p">.</span><span class="n">PyTricia</span><span class="p">()</span>
<span class="n">route_tree</span><span class="p">[</span><span class="err">’</span><span class="n">subnet</span><span class="err">’</span><span class="p">]</span> <span class="o">=</span> <span class="p">((</span><span class="n">next_hop_1</span><span class="p">,</span> <span class="n">next_hop_n</span><span class="p">),</span> <span class="n">raw_route_string</span><span class="p">)</span>
<span class="n">interface_list</span> <span class="o">=</span> <span class="p">((</span><span class="n">interface_1</span><span class="p">,</span> <span class="n">ip_address_1</span><span class="p">),</span> <span class="p">(</span><span class="n">interface_n</span><span class="p">,</span> <span class="n">ip_address_n</span><span class="p">))</span>
<span class="n">connected_networks</span> <span class="o">=</span> <span class="p">((</span><span class="n">interface_1</span><span class="p">,</span> <span class="n">subnet_1</span><span class="p">),</span> <span class="p">(</span><span class="n">interface_n</span><span class="p">,</span> <span class="n">subnet_n</span><span class="p">))</span>
<span class="n">router</span> <span class="o">=</span> <span class="p">{</span>
    <span class="err">‘</span><span class="n">routing_table</span><span class="err">’</span><span class="p">:</span> <span class="n">route_tree</span><span class="p">,</span>
    <span class="err">‘</span><span class="n">interface_list</span><span class="err">’</span><span class="p">:</span> <span class="n">interface_list</span><span class="p">,</span>
    <span class="err">‘</span><span class="n">connected_networks</span><span class="err">’</span><span class="p">:</span> <span class="n">connected_networks</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now we can implement a route lookup function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">route_lookup</span><span class="p">(</span><span class="n">destination</span><span class="p">,</span> <span class="n">router</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">destination</span> <span class="ow">in</span> <span class="n">router</span><span class="p">[</span><span class="s">'routing_table'</span><span class="p">]:</span>
        <span class="k">return</span> <span class="n">router</span><span class="p">[</span><span class="s">'routing_table'</span><span class="p">][</span><span class="n">destination</span><span class="p">]</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
</code></pre></div></div>

<p>To distinguish between the routers, it is important to assign some unique router identifier (RID) for each of them. Router ID generation and selection algorithms might be different. In our case, let’s use a filename as a RID for simplicity.<br />
Let’s put all resulting routers into a dictionary with RIDs as keys and corresponding router objects as values:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ROUTERS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="err">‘</span><span class="n">router_id_1</span><span class="err">’</span><span class="p">:</span> <span class="n">router_1</span><span class="p">,</span>
    <span class="err">‘</span><span class="n">router_id_n</span><span class="err">’</span><span class="p">:</span> <span class="n">router_n</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We also need to implement some next-hop RID resolution mechanism by known next-hop IP-address (think of ARP).
Let’s create one more prefix tree containing IP-addresses of all discovered router as keys and RID with interface types as corresponding values:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example
</span><span class="n">GLOBAL_INTERFACE_TREE</span> <span class="o">=</span> <span class="n">pytricia</span><span class="p">.</span><span class="n">PyTricia</span><span class="p">()</span>
<span class="n">GLOBAL_INTERFACE_TREE</span><span class="p">[</span><span class="err">‘</span><span class="n">ip_address</span><span class="err">’</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">router_id</span><span class="p">,</span> <span class="n">interface_type</span><span class="p">)</span>

<span class="c1"># Returns RouterID by Interface IP address which it belongs to.
</span><span class="k">def</span> <span class="nf">get_rid_by_interface_ip</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">interface_ip</span> <span class="ow">in</span> <span class="n">GLOBAL_INTERFACE_TREE</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">GLOBAL_INTERFACE_TREE</span><span class="p">[</span><span class="n">interface_ip</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>Now let’s combine our IOS/IOS-XE/ASA format parsers into a single function. It will take a text routing table as an input and return a router dictionary object of a format we discussed earlier:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_show_ip_route_ios_like</span><span class="p">(</span><span class="n">raw_routing_table</span><span class="p">):</span>
    <span class="s">"""
    Parser for routing table text output.
    Compatible with both Cisco IOS(IOS-XE) 'show ip route'
    and Cisco ASA 'show route' output format.
    Processes input text file and write into Python data structures.
    Builds internal PyTricia search tree in 'route_tree'.
    Generates local interface list for a router in 'interface_list'
    Returns 'router' dictionary object with parsed data.
    """</span>
    <span class="n">router</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">route_tree</span> <span class="o">=</span> <span class="n">pytricia</span><span class="p">.</span><span class="n">PyTricia</span><span class="p">()</span>
    <span class="n">interface_list</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="c1"># Parse Local and Connected route strings in text.
</span>    <span class="k">for</span> <span class="n">raw_route_string</span> <span class="ow">in</span> <span class="n">REGEXP_ROUTE_LOCAL_CONNECTED</span><span class="p">.</span><span class="n">finditer</span><span class="p">(</span><span class="n">raw_routing_table</span><span class="p">):</span>
        <span class="n">subnet</span> <span class="o">=</span> <span class="p">(</span>
            <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'ipaddress'</span><span class="p">)</span>
            <span class="o">+</span> <span class="n">convert_netmask_to_prefix_length</span><span class="p">(</span>
                <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'maskOrPrefixLength'</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="p">)</span>
        <span class="n">interface</span> <span class="o">=</span> <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'interface'</span><span class="p">)</span>
        <span class="n">route_tree</span><span class="p">[</span><span class="n">subnet</span><span class="p">]</span> <span class="o">=</span> <span class="p">((</span><span class="n">interface</span><span class="p">,),</span> <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
        <span class="k">if</span> <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'routeType'</span><span class="p">)</span> <span class="o">==</span> <span class="s">'L'</span><span class="p">:</span>
            <span class="n">interface_list</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">interface</span><span class="p">,</span> <span class="n">subnet</span><span class="p">,))</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">interface_list</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'Failed to find routing table entries in given output'</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>
    <span class="c1"># parse static and dynamic route strings in text
</span>    <span class="k">for</span> <span class="n">raw_route_string</span> <span class="ow">in</span> <span class="n">REGEXP_ROUTE</span><span class="p">.</span><span class="n">finditer</span><span class="p">(</span><span class="n">raw_routing_table</span><span class="p">):</span>
        <span class="n">subnet</span> <span class="o">=</span> <span class="p">(</span>
            <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'subnet'</span><span class="p">)</span>
            <span class="o">+</span> <span class="n">convert_netmask_to_prefix_length</span><span class="p">(</span>
                <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'maskOrPrefixLength'</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="p">)</span>
        <span class="n">via_portion</span> <span class="o">=</span> <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="s">'viaPortion'</span><span class="p">)</span>
        <span class="n">next_hops</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">if</span> <span class="n">via_portion</span><span class="p">.</span><span class="n">count</span><span class="p">(</span><span class="s">'via'</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">via_portion</span><span class="p">.</span><span class="n">splitlines</span><span class="p">():</span>
                <span class="k">if</span> <span class="n">line</span><span class="p">:</span>
                    <span class="n">next_hops</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">REGEXP_VIA_PORTION</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="n">line</span><span class="p">).</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">next_hops</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">REGEXP_VIA_PORTION</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="n">via_portion</span><span class="p">).</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
        <span class="n">route_tree</span><span class="p">[</span><span class="n">subnet</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">next_hops</span><span class="p">,</span> <span class="n">raw_route_string</span><span class="p">.</span><span class="n">group</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
    <span class="n">router</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'routing_table'</span><span class="p">:</span> <span class="n">route_tree</span><span class="p">,</span>
        <span class="s">'interface_list'</span><span class="p">:</span> <span class="n">interface_list</span><span class="p">,</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">router</span>
</code></pre></div></div>
<p>To improve extensibility, let’s wrap all parsers into another function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_text_routing_table</span><span class="p">(</span><span class="n">raw_routing_table</span><span class="p">):</span>
    <span class="s">"""
    Parser functions wrapper.
    Add additional parsers for alternative routing table syntaxes here.
    """</span>
    <span class="n">router</span> <span class="o">=</span> <span class="n">parse_show_ip_route_ios_like</span><span class="p">(</span><span class="n">raw_routing_table</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">router</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">router</span>
</code></pre></div></div>

<p>Finally, we need a function to go through a directory containing our routing table text files.
It will take a directory path as an input and return a dictionary of all discovered routers:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">do_parse_directory</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">):</span>
    <span class="s">"""
    Go through the specified directory and parse all .txt files.
    Generate router objects based on parse result if any.
    Populate new_routers with those router objects.
    The default key for each router object is FILENAME.
    Return new_routers.
    """</span>
    <span class="n">new_routers</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">isdir</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">):</span>
        <span class="k">print</span><span class="p">(</span>
            <span class="s">"{} directory does not exist."</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">)</span>
            <span class="o">+</span> <span class="s">"Check rt_directory variable value."</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="bp">None</span>
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">()</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Initializing files..."</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">FILENAME</span> <span class="ow">in</span> <span class="n">os</span><span class="p">.</span><span class="n">listdir</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">FILENAME</span><span class="p">.</span><span class="n">endswith</span><span class="p">(</span><span class="s">'.txt'</span><span class="p">):</span>
            <span class="n">file_init_start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">()</span>
            <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">,</span> <span class="n">FILENAME</span><span class="p">),</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="k">print</span><span class="p">(</span><span class="s">'Opening {}'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">FILENAME</span><span class="p">))</span>
                <span class="n">raw_table</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
                <span class="n">new_router</span> <span class="o">=</span> <span class="n">parse_text_routing_table</span><span class="p">(</span><span class="n">raw_table</span><span class="p">)</span>
                <span class="n">router_id</span> <span class="o">=</span> <span class="n">FILENAME</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'.txt'</span><span class="p">,</span> <span class="s">''</span><span class="p">)</span>
                <span class="k">if</span> <span class="n">new_router</span><span class="p">:</span>
                    <span class="n">new_routers</span><span class="p">[</span><span class="n">router_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">new_router</span>
                    <span class="k">if</span> <span class="n">new_router</span><span class="p">[</span><span class="s">'interface_list'</span><span class="p">]:</span>
                        <span class="k">for</span> <span class="n">iface</span><span class="p">,</span> <span class="n">addr</span> <span class="ow">in</span> <span class="n">new_router</span><span class="p">[</span><span class="s">'interface_list'</span><span class="p">]:</span>
                            <span class="n">GLOBAL_INTERFACE_TREE</span><span class="p">[</span><span class="n">addr</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">router_id</span><span class="p">,</span> <span class="n">iface</span><span class="p">,)</span>
                <span class="k">else</span><span class="p">:</span>
                    <span class="k">print</span><span class="p">(</span><span class="s">'Failed to parse '</span> <span class="o">+</span> <span class="n">FILENAME</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span>
                <span class="n">FILENAME</span>
                <span class="o">+</span> <span class="s">" parsing has been completed in {} sec"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span>
                    <span class="s">"{:.3f}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">file_init_start_time</span><span class="p">)</span>
                <span class="p">)</span>
            <span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">new_routers</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span>
                <span class="s">"Could not find any valid .txt files with routing tables"</span>
                <span class="o">+</span> <span class="s">" in {} directory"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">rt_directory</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span>
                <span class="s">"</span><span class="se">\n</span><span class="s">All files have been initialized"</span>
                <span class="o">+</span> <span class="s">" in {} sec"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">"{:.3f}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span><span class="p">))</span>
            <span class="p">)</span>
            <span class="k">return</span> <span class="n">new_routers</span>
</code></pre></div></div>
<p>Once we have the structured data, we can move to the routing paths analysis part of the task.</p>

<h2 id="analyzing-routing-paths">Analyzing Routing Paths</h2>

<p>In general, the task at this stage is to analyze the network graph. Routers are graph vertices and L3-links are graph edges. <em>ROUTERS</em> dictionary stores Router IDs as keys and next-hop IP-addresses as values. <em>GLOBAL_INTERFACE_TREE</em> returns RIDs by next-hop IP-addresses at the same time. So <em>ROUTERS</em> and <em>GLOBAL_INTERFACE_TREE</em> together define a graph adjacency table.</p>

<p>If we draw parallels with real routers, to find a path, you need to reproduce their high-level work logic (not taking RIB/FIB/ASIC and different optimizations into account) during the packet processing: from routing table lookup to ARP-request (<em>router_id</em> in our case) and further packet forwarding or drop depending on the result.</p>

<p>To achieve this, let’s implement a recursive path search algorithm. Each path segment will be represented by a list containing <em>router_id</em> (RID) and <em>raw_route_string</em> (matched route string). The current path will be stored in a <em>path</em> tuple. As we might have multiple paths, the resulting list of them will be stored in a <em>paths</em> tuple. Individual <em>path</em> will be appended to <em>paths</em> once the current path analysis reached the end (the destination or no route to the destination at some point) or on routing loop detection. The function will take a RID we start from and a target IP we are searching path to as an input and return resulting <em>paths</em>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">trace_route</span><span class="p">(</span><span class="n">source_router_id</span><span class="p">,</span> <span class="n">target_ip</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="p">[]):</span>
    <span class="s">"""
    Performs recursive path search from source Router ID (RID) to target subnet.
    Returns tuple of path tuples.
    Each path tuple contains a sequence of Router IDs with matched route strings.
    Multiple paths are supported.
    """</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">source_router_id</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">path</span> <span class="o">+</span> <span class="p">[(</span><span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">)]]</span>
    <span class="n">current_router</span> <span class="o">=</span> <span class="n">ROUTERS</span><span class="p">[</span><span class="n">source_router_id</span><span class="p">]</span>
    <span class="n">next_hop</span><span class="p">,</span> <span class="n">raw_route_string</span> <span class="o">=</span> <span class="n">route_lookup</span><span class="p">(</span><span class="n">target_ip</span><span class="p">,</span> <span class="n">current_router</span><span class="p">)</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">path</span> <span class="o">+</span> <span class="p">[(</span><span class="n">source_router_id</span><span class="p">,</span> <span class="n">raw_route_string</span><span class="p">)]</span>
    <span class="n">paths</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">if</span> <span class="n">next_hop</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">nexthop_is_local</span><span class="p">(</span><span class="n">next_hop</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
            <span class="k">return</span> <span class="p">[</span><span class="n">path</span><span class="p">]</span>
        <span class="k">for</span> <span class="n">nh</span> <span class="ow">in</span> <span class="n">next_hop</span><span class="p">:</span>
            <span class="n">next_hop_rid</span> <span class="o">=</span> <span class="n">get_rid_by_interface_ip</span><span class="p">(</span><span class="n">nh</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">next_hop_rid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">path</span><span class="p">]:</span>
                <span class="n">inner_path</span> <span class="o">=</span> <span class="n">trace_route</span><span class="p">(</span><span class="n">next_hop_rid</span><span class="p">,</span> <span class="n">target_ip</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">inner_path</span><span class="p">:</span>
                    <span class="n">paths</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">path</span> <span class="o">=</span> <span class="n">path</span> <span class="o">+</span> <span class="p">[(</span><span class="n">next_hop_rid</span><span class="o">+</span><span class="s">"&lt;&lt;LOOP DETECTED"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)]</span>
                <span class="k">return</span> <span class="p">[</span><span class="n">path</span><span class="p">]</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">path</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">paths</span>


<span class="k">def</span> <span class="nf">nexthop_is_local</span><span class="p">(</span><span class="n">next_hop</span><span class="p">):</span>
    <span class="s">"""
    Check if next-hop points to the local interface.
    Will be True for Connected and Local route strings on Cisco devices.
    """</span>
    <span class="n">interface_types</span> <span class="o">=</span> <span class="p">(</span>
        <span class="s">'Eth'</span><span class="p">,</span> <span class="s">'Fast'</span><span class="p">,</span> <span class="s">'Gig'</span><span class="p">,</span> <span class="s">'Ten'</span><span class="p">,</span> <span class="s">'Port'</span><span class="p">,</span>
        <span class="s">'Serial'</span><span class="p">,</span> <span class="s">'Vlan'</span><span class="p">,</span> <span class="s">'Tunn'</span><span class="p">,</span> <span class="s">'Loop'</span><span class="p">,</span> <span class="s">'Null'</span>
    <span class="p">)</span>
    <span class="k">for</span> <span class="nb">type</span> <span class="ow">in</span> <span class="n">interface_types</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">next_hop</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="nb">type</span><span class="p">):</span>
            <span class="k">return</span> <span class="bp">True</span>
</code></pre></div></div>

<p>Let’s also implement a function to provide interactive path lookup ability to our script user.
It will perform a path search to the given IP-address from all the discovered routers:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">do_user_interactive_search</span><span class="p">():</span>
    <span class="s">"""
    Provides interactive search dialog for users.
    Asks user for target subnet or host in CIDR notation.
    Validates input. Prints error and goes back to start for invalid input.
    Executes path search to given target from each router in global ROUTERS.
    Prints formatted path search results.
    Goes back to start.
    """</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span>
        <span class="n">target_subnet</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s">'Enter Target Subnet or Host: '</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">target_subnet</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">REGEXP_INPUT_IPv4</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="n">target_subnet</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">' '</span><span class="p">,</span> <span class="s">''</span><span class="p">)):</span>
            <span class="k">print</span><span class="p">(</span><span class="s">"incorrect input"</span><span class="p">)</span>
            <span class="k">continue</span>
        <span class="n">lookup_start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">rtr</span> <span class="ow">in</span> <span class="n">ROUTERS</span><span class="p">.</span><span class="n">keys</span><span class="p">():</span>
            <span class="n">subsearch_start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">()</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">trace_route</span><span class="p">(</span><span class="n">rtr</span><span class="p">,</span> <span class="n">target_subnet</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
                <span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
                <span class="k">print</span><span class="p">(</span><span class="s">"PATHS TO {} FROM {}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">target_subnet</span><span class="p">,</span> <span class="n">rtr</span><span class="p">))</span>
                <span class="n">n</span> <span class="o">=</span> <span class="mi">1</span>
                <span class="k">print</span><span class="p">(</span><span class="s">'Detailed info:'</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">result</span><span class="p">:</span>
                    <span class="k">print</span><span class="p">(</span><span class="s">"Path {}:"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">n</span><span class="p">))</span>
                    <span class="k">print</span><span class="p">([</span><span class="n">h</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="n">r</span><span class="p">])</span>
                    <span class="k">for</span> <span class="n">hop</span> <span class="ow">in</span> <span class="n">r</span><span class="p">:</span>
                        <span class="k">print</span><span class="p">(</span><span class="s">"ROUTER: {}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">hop</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>
                        <span class="k">print</span><span class="p">(</span><span class="s">"Matched route string: </span><span class="se">\n</span><span class="s">{}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">hop</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span>
                    <span class="k">else</span><span class="p">:</span>
                        <span class="k">print</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span>
                    <span class="n">n</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="k">else</span><span class="p">:</span>
                    <span class="k">print</span><span class="p">(</span>
                        <span class="s">"Path search on {} has been completed in {} sec"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span>
                           <span class="n">rtr</span><span class="p">,</span> <span class="s">"{:.3f}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">subsearch_start_time</span><span class="p">)</span>
                        <span class="p">)</span>
                    <span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span>
                <span class="s">"</span><span class="se">\n</span><span class="s">Full search has been completed in {} sec"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span>
                   <span class="s">"{:.3f}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">lookup_start_time</span><span class="p">),</span>
                <span class="p">)</span>
            <span class="p">)</span>
</code></pre></div></div>

<p>Bringing the logic together:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">global</span> <span class="n">ROUTERS</span>
    <span class="n">ROUTERS</span> <span class="o">=</span> <span class="n">do_parse_directory</span><span class="p">(</span><span class="n">RT_DIRECTORY</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">ROUTERS</span><span class="p">:</span>
        <span class="n">do_user_interactive_search</span><span class="p">()</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span> 
</code></pre></div></div>

<p>And here is a <a href="https://github.com/iDebugAll/toolz/blob/master/traceroute_by_routing_tables/traceroute_by_routing_tables.py">complete solution</a>!</p>

<details>
  <summary>
    The Code.
  </summary>
 
    <script src="https://gist.github.com/iDebugAll/feef8e4339c66a92bb15ec1f87165075.js"></script>

</details>

<p>How do we not it is working? Of course, let’s do some testing.</p>

<h2 id="testing">Testing</h2>

<p>I used a small topology consisting of four Cisco <a href="https://www.cisco.com/c/en/us/products/collateral/routers/cloud-services-router-1000v-series/datasheet-c78-733443.html">CSR-1000v</a> routers for testing:
<img src="https://habrastorage.org/webt/qi/ga/qn/qigaqn1qe-wztcio2uqg9jtbmpe.jpeg" /></p>

<p>They are interconnected with GigabitEthernet 2 and 3 interfaces. All adjacent routers are EIGRP neighbors. All Connected networks are being advertised, including Loopback addresses behind each router. Besides, csr1000v-01 and csr1000v-04 have a pair of GRE tunnels between them. Both of them have a static route for 10.0.0.0/8 subnet pointing to remote GRE tunnel IP forming a routing loop.</p>

<details>
<summary>csr1000v-01#show ip route</summary>

<pre>
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

S     10.0.0.0/8 [1/0] via 192.168.142.2
                 [1/0] via 192.168.141.2
      172.16.0.0/16 is variably subnetted, 2 subnets, 2 masks
C        172.16.114.0/24 is directly connected, GigabitEthernet2
L        172.16.114.5/32 is directly connected, GigabitEthernet2
      192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.2.0/24 is directly connected, GigabitEthernet1
L        192.168.2.201/32 is directly connected, GigabitEthernet1
      192.168.12.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.12.0/24 is directly connected, GigabitEthernet2
L        192.168.12.201/32 is directly connected, GigabitEthernet2
      192.168.13.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.13.0/24 is directly connected, GigabitEthernet3
L        192.168.13.201/32 is directly connected, GigabitEthernet3
D     192.168.24.0/24 [90/3072] via 192.168.12.202, 00:06:56, GigabitEthernet2
D     192.168.34.0/24 [90/3072] via 192.168.13.203, 00:06:56, GigabitEthernet3
      192.168.141.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.141.0/30 is directly connected, Tunnel141
L        192.168.141.1/32 is directly connected, Tunnel141
      192.168.142.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.142.0/30 is directly connected, Tunnel142
L        192.168.142.1/32 is directly connected, Tunnel142
      192.168.201.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.201.0/24 is directly connected, Loopback201
L        192.168.201.201/32 is directly connected, Loopback201
D     192.168.202.0/24 
           [90/130816] via 192.168.12.202, 00:05:44, GigabitEthernet2
D     192.168.203.0/24 
           [90/130816] via 192.168.13.203, 00:06:22, GigabitEthernet3
D     192.168.204.0/24 
           [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3
           [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2
</pre>

</details>

<details>
<summary>csr1000v-02#show ip route</summary>

<pre>
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.2.0/24 is directly connected, GigabitEthernet1
L        192.168.2.202/32 is directly connected, GigabitEthernet1
      192.168.12.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.12.0/24 is directly connected, GigabitEthernet2
L        192.168.12.202/32 is directly connected, GigabitEthernet2
D     192.168.13.0/24 [90/3072] via 192.168.12.201, 00:46:17, GigabitEthernet2
      192.168.24.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.24.0/24 is directly connected, GigabitEthernet3
L        192.168.24.202/32 is directly connected, GigabitEthernet3
D     192.168.34.0/24 [90/3072] via 192.168.24.204, 00:46:15, GigabitEthernet3
D     192.168.201.0/24 
           [90/130816] via 192.168.12.201, 00:36:59, GigabitEthernet2
      192.168.202.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.202.0/24 is directly connected, Loopback202
L        192.168.202.202/32 is directly connected, Loopback202
D     192.168.203.0/24 
           [90/131072] via 192.168.24.204, 00:06:31, GigabitEthernet3
           [90/131072] via 192.168.12.201, 00:06:31, GigabitEthernet2
D     192.168.204.0/24 
           [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3
</pre>

</details>

<details>
<summary>csr1000v-03#show ip route</summary>

<pre>
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.2.0/24 is directly connected, GigabitEthernet1
L        192.168.2.203/32 is directly connected, GigabitEthernet1
D     192.168.12.0/24 [90/3072] via 192.168.13.201, 00:46:12, GigabitEthernet3
      192.168.13.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.13.0/24 is directly connected, GigabitEthernet3
L        192.168.13.203/32 is directly connected, GigabitEthernet3
D     192.168.24.0/24 [90/3072] via 192.168.34.204, 00:46:12, GigabitEthernet2
      192.168.34.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.34.0/24 is directly connected, GigabitEthernet2
L        192.168.34.203/32 is directly connected, GigabitEthernet2
D     192.168.201.0/24 
           [90/130816] via 192.168.13.201, 00:36:56, GigabitEthernet3
D     192.168.202.0/24 
           [90/131072] via 192.168.34.204, 00:05:51, GigabitEthernet2
           [90/131072] via 192.168.13.201, 00:05:51, GigabitEthernet3
      192.168.203.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.203.0/24 is directly connected, Loopback203
L        192.168.203.203/32 is directly connected, Loopback203
D     192.168.204.0/24 
           [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2
</pre>

</details>

<details>
<summary>csr1000v-04#show ip route</summary>

<pre>
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

S     10.0.0.0/8 [1/0] via 192.168.142.1
                 [1/0] via 192.168.141.1
      192.168.2.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.2.0/24 is directly connected, GigabitEthernet1
L        192.168.2.204/32 is directly connected, GigabitEthernet1
D     192.168.12.0/24 [90/3072] via 192.168.24.202, 00:46:17, GigabitEthernet3
D     192.168.13.0/24 [90/3072] via 192.168.34.203, 00:46:19, GigabitEthernet2
      192.168.24.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.24.0/24 is directly connected, GigabitEthernet3
L        192.168.24.204/32 is directly connected, GigabitEthernet3
      192.168.34.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.34.0/24 is directly connected, GigabitEthernet2
L        192.168.34.204/32 is directly connected, GigabitEthernet2
      192.168.141.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.141.0/30 is directly connected, Tunnel141
L        192.168.141.2/32 is directly connected, Tunnel141
      192.168.142.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.142.0/30 is directly connected, Tunnel142
L        192.168.142.2/32 is directly connected, Tunnel142
D     192.168.201.0/24 
           [90/131072] via 192.168.34.203, 00:37:02, GigabitEthernet2
           [90/131072] via 192.168.24.202, 00:37:02, GigabitEthernet3
D     192.168.202.0/24 
           [90/130816] via 192.168.24.202, 00:05:57, GigabitEthernet3
D     192.168.203.0/24 
           [90/130816] via 192.168.34.203, 00:06:34, GigabitEthernet2
      192.168.204.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.204.0/24 is directly connected, Loopback204
L        192.168.204.204/32 is directly connected, Loopback204
</pre>

</details>

<p>Let’s save <em>show ip route</em> files into separate files named by hostnames inside <em>./routing_tables/</em> directory.
Now we can run the script:</p>

<details>
<summary>$ python3.7 traceroute_by_routing_tables.py</summary>

<pre>
$ python3.7 traceroute_by_routing_tables.py
Initializing files...
Opening csr1000v-01.txt
csr1000v-01.txt parsing has been completed in 0.001 sec
Opening csr1000v-02.txt
csr1000v-02.txt parsing has been completed in 0.001 sec
Opening csr1000v-03.txt
csr1000v-03.txt parsing has been completed in 0.001 sec
Opening csr1000v-04.txt
csr1000v-04.txt parsing has been completed in 0.001 sec

All files have been initialized in 0.003 sec


Enter Target Subnet or Host:
</pre>

</details>

<p>All the files are processed as expected. The script expects an IP-address input to analyze paths.
Let’s put several IP-addresses subsequently and compare the output with the data from our routers:</p>

<details>
<summary>Looking up paths to 192.168.204.204 (Loopback204 on csr1000v-04)</summary>

All the routers should have paths to this destination.

<details>
<summary>Enter Target Subnet or Host: 192.168.204.204</summary>

<pre>
Enter Target Subnet or Host: 192.168.204.204


PATHS TO 192.168.204.204 FROM csr1000v-04
Detailed info:
Path 1:
['csr1000v-04']
ROUTER: csr1000v-04
Matched route string: 
L        192.168.204.204/32 is directly connected, Loopback204


Path search on csr1000v-04 has been completed in 0.000 sec


PATHS TO 192.168.204.204 FROM csr1000v-03
Detailed info:
Path 1:
['csr1000v-03', 'csr1000v-04']
ROUTER: csr1000v-03
Matched route string: 
D     192.168.204.0/24 
           [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2

ROUTER: csr1000v-04
Matched route string: 
L        192.168.204.204/32 is directly connected, Loopback204


Path search on csr1000v-03 has been completed in 0.000 sec


PATHS TO 192.168.204.204 FROM csr1000v-02
Detailed info:
Path 1:
['csr1000v-02', 'csr1000v-04']
ROUTER: csr1000v-02
Matched route string: 
D     192.168.204.0/24 
           [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3
ROUTER: csr1000v-04
Matched route string: 
L        192.168.204.204/32 is directly connected, Loopback204


Path search on csr1000v-02 has been completed in 0.000 sec


PATHS TO 192.168.204.204 FROM csr1000v-01
Detailed info:
Path 1:
['csr1000v-01', 'csr1000v-03', 'csr1000v-04']
ROUTER: csr1000v-01
Matched route string: 
D     192.168.204.0/24 
           [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3
           [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2
ROUTER: csr1000v-03
Matched route string: 
D     192.168.204.0/24 
           [90/130816] via 192.168.34.204, 00:37:22, GigabitEthernet2

ROUTER: csr1000v-04
Matched route string: 
L        192.168.204.204/32 is directly connected, Loopback204


Path 2:
['csr1000v-01', 'csr1000v-02', 'csr1000v-04']
ROUTER: csr1000v-01
Matched route string: 
D     192.168.204.0/24 
           [90/131072] via 192.168.13.203, 00:06:56, GigabitEthernet3
           [90/131072] via 192.168.12.202, 00:06:56, GigabitEthernet2
ROUTER: csr1000v-02
Matched route string: 
D     192.168.204.0/24 
           [90/130816] via 192.168.24.204, 00:37:26, GigabitEthernet3
ROUTER: csr1000v-04
Matched route string: 
L        192.168.204.204/32 is directly connected, Loopback204


Path search on csr1000v-01 has been completed in 0.000 sec

Full search has been completed in 0.001 sec
</pre>

</details>

The script found some paths. Now let's check the route selection right on csr1000v-01:

<details>
<summary>csr1000v-01#show ip route 192.168.204.204</summary>

<pre>
csr1000v-01#show ip route 192.168.204.204
Routing entry for 192.168.204.0/24
  Known via "eigrp 200", distance 90, metric 131072, type internal
  Redistributing via eigrp 200
  Last update from 192.168.13.203 on GigabitEthernet3, 00:02:15 ago
  Routing Descriptor Blocks:
    192.168.13.203, from 192.168.13.203, 00:02:15 ago, via GigabitEthernet3
      Route metric is 131072, traffic share count is 1
      Total delay is 5020 microseconds, minimum bandwidth is 1000000 Kbit
      Reliability 255/255, minimum MTU 1500 bytes
      Loading 1/255, Hops 2
  * 192.168.12.202, from 192.168.12.202, 00:02:15 ago, via GigabitEthernet2
      Route metric is 131072, traffic share count is 1
      Total delay is 5020 microseconds, minimum bandwidth is 1000000 Kbit
      Reliability 255/255, minimum MTU 1500 bytes
      Loading 1/255, Hops 2
</pre>

</details>

csr1000v-01 displays two equal-cost router learned by EIGRP through csr1000v-02 and csr1000v-03. 
The script returns both available paths: ['csr1000v-01', 'csr1000v-03', 'csr1000v-04'] and ['csr1000v-01', 'csr1000v-02', 'csr1000v-04'].<br />

To be sure:

<details>
<summary>csr1000v-02#show ip route 192.168.204.204</summary>

<pre>
csr1000v-02#show ip route 192.168.204.204
Routing entry for 192.168.204.0/24
  Known via "eigrp 200", distance 90, metric 130816, type internal
  Redistributing via eigrp 200
  Last update from 192.168.24.204 on GigabitEthernet3, 00:08:48 ago
  Routing Descriptor Blocks:
  * 192.168.24.204, from 192.168.24.204, 00:08:48 ago, via GigabitEthernet3
      Route metric is 130816, traffic share count is 1
      Total delay is 5010 microseconds, minimum bandwidth is 1000000 Kbit
      Reliability 255/255, minimum MTU 1500 bytes
      Loading 1/255, Hops 1
</pre>

</details>

<details>
<summary>csr1000v-03#show ip route 192.168.204.204</summary>

<pre>
csr1000v-3#show ip route 192.168.204.204
Routing entry for 192.168.204.0/24
  Known via "eigrp 200", distance 90, metric 130816, type internal
  Redistributing via eigrp 200
  Last update from 192.168.34.204 on GigabitEthernet2, 00:08:45 ago
  Routing Descriptor Blocks:
  * 192.168.34.204, from 192.168.34.204, 00:08:45 ago, via GigabitEthernet2
      Route metric is 130816, traffic share count is 1
      Total delay is 5010 microseconds, minimum bandwidth is 1000000 Kbit
      Reliability 255/255, minimum MTU 1500 bytes
      Loading 1/255, Hops 1
</pre>

</details>

<details>
<summary>csr1000v-04#show ip route 192.168.204.204</summary>

<pre>
csr1000v-04#show ip route 192.168.204.204
Routing entry for 192.168.204.204/32
  Known via "connected", distance 0, metric 0 (connected)
  Routing Descriptor Blocks:
  * directly connected, via Loopback204
      Route metric is 0, traffic share count is 1
</pre>

</details>

Both csr1000v-02 and csr1000v-03 have a single route learned by EIGRP to csr1000v-4.
On csr1000v-04, the route leads to a Connected network on Loopback204.
The script output is correct: ['csr1000v-02', 'csr1000v-04'] from csr1000v-02, ['csr1000v-03', 'csr1000v-04'] from csr1000v-03, and a route to itself ['csr1000v-04'] from csr1000v-04.
</details>

<details>
<summary>Looking up 10.10.10.0/24 (does not exist in the topology). Also a routing loop test case.</summary>

<details>
<summary>Enter Target Subnet or Host: 10.10.10.0/24</summary>

<pre>
Enter Target Subnet or Host: 10.10.10.0/24

PATHS TO 10.10.10.0/24 FROM csr1000v-04
Detailed info:
Path 1:
['csr1000v-04', 'csr1000v-01', 'csr1000v-04&lt;&lt;LOOP DETECTED']
ROUTER: csr1000v-04
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.1
                 [1/0] via 192.168.141.1

ROUTER: csr1000v-01
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.2
                 [1/0] via 192.168.141.2

ROUTER: csr1000v-04&lt;&lt;LOOP DETECTED
Matched route string: 
None


Path 2:
['csr1000v-04', 'csr1000v-01', 'csr1000v-04&lt;&lt;LOOP DETECTED']
ROUTER: csr1000v-04
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.1
                 [1/0] via 192.168.141.1

ROUTER: csr1000v-01
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.2
                 [1/0] via 192.168.141.2

ROUTER: csr1000v-04&lt;&lt;LOOP DETECTED
Matched route string: 
None


Path search on csr1000v-04 has been completed in 0.000 sec


PATHS TO 10.10.10.0/24 FROM csr1000v-03
Detailed info:
Path 1:
['csr1000v-03']
ROUTER: csr1000v-03
Matched route string: 
None


Path search on csr1000v-03 has been completed in 0.000 sec


PATHS TO 10.10.10.0/24 FROM csr1000v-02
Detailed info:
Path 1:
['csr1000v-02']
ROUTER: csr1000v-02
Matched route string: 
None


Path search on csr1000v-02 has been completed in 0.000 sec


PATHS TO 10.10.10.0/24 FROM csr1000v-01
Detailed info:
Path 1:
['csr1000v-01', 'csr1000v-04', 'csr1000v-01&lt;&lt;LOOP DETECTED']
ROUTER: csr1000v-01
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.2
                 [1/0] via 192.168.141.2

ROUTER: csr1000v-04
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.1
                 [1/0] via 192.168.141.1

ROUTER: csr1000v-01&lt;&lt;LOOP DETECTED
Matched route string: 
None


Path 2:
['csr1000v-01', 'csr1000v-04', 'csr1000v-01&lt;&lt;LOOP DETECTED']
ROUTER: csr1000v-01
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.2
                 [1/0] via 192.168.141.2

ROUTER: csr1000v-04
Matched route string: 
S     10.0.0.0/8 [1/0] via 192.168.142.1
                 [1/0] via 192.168.141.1

ROUTER: csr1000v-01&lt;&lt;LOOP DETECTED
Matched route string: 
None


Path search on csr1000v-01 has been completed in 0.003 sec

Full search has been completed in 0.004 sec
</pre>

</details>

We've got result. Let's check the routers:

<details>
<summary>csr1000v-01#show ip route 10.10.10.0 255.255.255.0</summary>

<pre>
csr1000v-01#show ip route 10.10.10.0 255.255.255.0
Routing entry for 10.0.0.0/8
  Known via "static", distance 1, metric 0
  Routing Descriptor Blocks:
  * 192.168.142.2
      Route metric is 0, traffic share count is 1
    192.168.141.2
      Route metric is 0, traffic share count is 1
</pre>

</details>

<details>
<summary>csr1000v-04#show ip route 10.10.10.0 255.255.255.0</summary>

<pre>
csr1000v-04#show ip route 10.10.10.0 255.255.255.0
Routing entry for 10.0.0.0/8
  Known via "static", distance 1, metric 0
  Routing Descriptor Blocks:
    192.168.142.1
      Route metric is 0, traffic share count is 1
  * 192.168.141.1
      Route metric is 0, traffic share count is 1
</pre>

</details>

As discussed, csr1000v-01 and csr1000v-04 have equal-cost static routes to a wide 10.0.0.0/8 network pointing to each other through the tunnel interfaces. It creates a routing loop. The script successfully detects this and shows both paths for each:

<pre>
PATHS TO 10.10.10.0/24 FROM csr1000v-01
Path 1:
['csr1000v-01', 'csr1000v-04', 'csr1000v-01&lt;&lt;LOOP DETECTED']
Path 2:
['csr1000v-01', 'csr1000v-04', 'csr1000v-01&lt;&lt;LOOP DETECTED']

PATHS TO 10.10.10.0/24 FROM csr1000v-04
Path 1:
['csr1000v-04', 'csr1000v-01', 'csr1000v-04&lt;&lt;LOOP DETECTED']
Path 2:
['csr1000v-04', 'csr1000v-01', 'csr1000v-04&lt;&lt;LOOP DETECTED']
</pre>

<details>
<summary>csr1000v-02#show ip route 10.10.10.0 255.255.255.0</summary>

<pre>
csr1000v-02#show ip route 10.10.10.0 255.255.255.0
% Network not in table
</pre>

</details>

<details>
<summary>csr1000v-3#show ip route 10.10.10.0 255.255.255.0</summary>

<pre>
csr1000v-3#show ip route 10.10.10.0 255.255.255.0
% Network not in table
</pre> 

</details>

csr1000v-02 and csr1000v-03 have no route to such destination. The script shows the same result.

</details>

<p>All common test cases are covered. The script result matches the output we get from the actual network devices using CLI.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The resulting solution is not perfect but it provides great lookup speed thanks to efficient algorithms and successfully solves the original task. The source code is published on my <a href="https://github.com/iDebugAll/toolz/tree/master/traceroute_by_routing_tables">GitHub page</a>.</p>

<p>The solution has some room for improvement and adding new analysis features. Additional parsers for alternative routing table syntax can easily be added by design. IPv6 is supported by <em>PyTricia</em> library natively.</p>

<p>I also tested the script on a BGP Full View routing table output with 700,000+ prefixes. On my good old MacBook Pro with Intel Core i5 and 8GB RAM, an initialization time takes less than 10 seconds. Memory consumption is around 320-350MB. Once it is initialized, any lookup to a routing table of that size still takes milliseconds as expected.</p>

<p>In 2021, some full-blown network analysis tools like <a href="https://developer.cisco.com/docs/pyats/">PyATS</a> and <a href="https://www.batfish.org">Batfish</a> could be a better starting point for more complex scenarios. However, being able to develop custom tools for such corner cases is still relevant.</p>

<p>Hope it helps someone solve some real issues or inspire to develop some automation on his or her own. 
Thank you for reading.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This post demonstrates an interesting use-case of radix tries usage for Longest Prefix Match. The post was born from a question asked by an IT forum member. The summary of the question looked as follows: There is a set of text files containing routing tables collected from various network devices. Each file represents one device. Device platforms and routing table formats may vary. It is required to analyze a routing path from any device to an arbitrary subnet or host on-demand. Resulting output should contain a list of routing table entries that are used for the routing to the given destination on each hop. The one who asked a question worked as a TAC engineer. It is often that they collect or receive from the customers some text ‘snapshots’ of the network state for further offline analysis while troubleshooting the issues. Some automation could really save a lot of time. I found this task interesting and also applicable to my own needs, so I decided to write a Proof-of-Concept implementation in Python 3 for Cisco IOS, IOS-XE, and ASA routing table format. In this article, I’ll try to reconstruct the resulting script development process and my considerations behind each step.]]></summary></entry><entry><title type="html">Visualizing Network Topologies: Zero to Hero in Two Days</title><link href="https://idebugall.github.io/visualize-lldp/" rel="alternate" type="text/html" title="Visualizing Network Topologies: Zero to Hero in Two Days" /><published>2020-12-24T00:00:00+00:00</published><updated>2020-12-24T00:00:00+00:00</updated><id>https://idebugall.github.io/visualize-lldp</id><content type="html" xml:base="https://idebugall.github.io/visualize-lldp/"><![CDATA[<p>This is a follow-up article on a local Cisco Russia DevNet Marathon online event I attended in May 2020. It was a series of educational webinars on network automation followed by daily challenges based on the discussed topics.<br />
On 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.<br /></p>

<p>The task was definitely not trivial and not widely covered in public blog posts. In this article, I would like to break down my own solution that finally took first place and describe the selected toolset and considerations.<br /><br /></p>

<p><img src="https://habrastorage.org/webt/5h/zi/p1/5hzip10y5vl8l71jpwwfmqp-xes.png" alt="" /></p>

<cut />

<hr />

<h3 id="disclaimer">Disclaimer</h3>

<blockquote>
  <p>This is a translation of my original article in Russian I posted in May 2020. If you are willing to help to improve the translation, please DM me.
The article does not aim to cover all the possible scenarios yet does describe the general approaches based on the specific case.
All above and below is an author’s personal subjective opinion if not stated otherwise.
All listed code is published under <a href="https://github.com/iDebugAll/devnet_marathon_endgame/blob/master/LICENSE">MIT</a> license and does not provide guarantees of any kind.</p>

  <p>The solution is written in Python and JavaScript. An understanding of the programming and networking basics is desirable for reading.</p>

  <p>If you found a typo, please use Ctrl+Enter or ⌘+Enter to send it to the author.</p>
</blockquote>

<h1 id="the-task">The Task</h1>
<p>The final task description was the following:</p>
<blockquote>

  <p>There is a network consisting of various L2/L3 network devices running IOS/IOS-XE. You have a list of management IP-addresses of all the devices. All devices are available by their IP addresses. You have access rights to execute any ‘show’ commands. You are free to use any data gathering methods. But trust us, you unlikely need SNMP. We are not ought to limit your fantasy though.</p>

  <p>The primary task is to identify the physical topology and device interconnections based on LLDP data and visualize it in a human-friendly format (yeah, we all find visual diagrams more readable). Then you should save the result in a format suitable for further analysis by the computer (yeah, machines are not that good at reading visual diagrams).</p>

  <p>The topology view should include:
<br /></p>
  <ul>
    <li>Different icons for each device type (routers and switches may have the same icon).</li>
    <li>Device hostnames.</li>
    <li>Interface names (you may use a shortened format, e.g. Gi0/0 for GigabitEthernet0/0/0).</li>
  </ul>

  <p>It is allowed to implement filters limiting or hiding some of the information.
A bonus task is to identify the topology changes (by comparing the current and the previous version) and visualize them in a human-friendly format.</p>
</blockquote>

<p>A summary: IP-addresses and credentials as an input, a visualized topology as an output (and a great space for experiments and options somewhere in between).
<br /></p>

<p>I also denoted some additional personal considerations to follow while choosing the solution toolset:</p>

<ul>
  <li>Feature-rich vs Simple balance.<br />
The solution should be balanced in terms of available features and the ease of their usage and implementation. Some ready-to-use open-source free tools might be used.</li>
  <li>Familiarity with the selected toolset.<br />
We have had three days to complete the task. The first day I had to spend on some side emergencies. So to be able to provide a working solution within such a limited time frame some known tools were a must.</li>
  <li>Solution reusability.<br />
The task is potentially applicable to many production networks. One should keep this in mind.</li>
  <li>Support for multiple network platforms.<br />
Real-world networks consist of many platforms. This is a thing to note as well.</li>
  <li>The documentation must be available for the chosen toolset. <br />
I believe this is a mandatory requirement for any reusable solution.</li>
</ul>

<h1 id="existing-solutions">Existing solutions</h1>
<p>It is always a good idea to check whether the wheel <a href="https://en.wikipedia.org/wiki/Not_invented_here">is already invented</a> before reinventing it by yourself. I have made some research on available existing solutions for network visualization. Not surprisingly, no solution could fit all the requirements out of the box. Most such solutions were built into much larger (and typically far not free) enterprise-grade network monitoring and analysis systems which would significantly reduce reusability and customization potential.</p>

<h1 id="task-decomposition-and-toolset-selection">Task decomposition and toolset selection</h1>

<p>I would divide an abstract network visualization task into the following layers and steps:
<img src="https://habrastorage.org/webt/aq/qk/ua/aqqkuairv5s0695ujmqtsffl8m0.png" alt="high_level" /></p>

<p>Let’s focus on each of them while keeping our requirement in mind:</p>

<ol>
  <li>
    <p><strong>Network Equipment</strong><br />
The initial task requires us to support IOS and IOS-XE.
Real-world networks tend to be much more heterogeneous. Let’s try to consider this.</p>
  </li>
  <li>
    <p><strong>Topology Data Sources</strong><br />
The task statement suggests us to use <a href="https://en.wikipedia.org/wiki/Link_Layer_Discovery_Protocol">LLDP</a> (Link Layer Discovery Protocol) protocol. This is an L2 (OSI Link Layer) protocol described in IEEE 802.1AB standard. It is widely supported by modern network platforms (including IOS and IOS-XE) and operating systems (including Linux and Window), so it suits our case.<br />
The topology data can also be enriched by various outputs from the network devices, such as routing and switching tables, routing protocols data, and so on. Let’s just mark it for future improvements.</p>
  </li>
  <li>
    <p><strong>Data Access Protocols and Standards</strong>
The most modern platforms usually support shiny and chrome <a href="https://en.wikipedia.org/wiki/NETCONF">NETCONF</a>, REST APIs, <a href="https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/169/b_169_programmability_cg/restconf_programmable_interface.html">RESTCONF</a> with <a href="https://en.wikipedia.org/wiki/YANG">YANG</a> models and data structures. The existence of legacy equipment and platforms will usually force us to revert to SSH, Telnet, and good ol’ CLI.</p>
  </li>
  <li>
    <p><strong>Protocol- and Vendor-Specific Drivers or Plugins</strong>
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 automation and this is a programming language I am most experienced in.<br />
API-driven network automation in Python can be done using the <strong>requests</strong> module or some specialized modules.<br />
Bare SSH/Telnet access to network equipment from Python commonly relies on <strong>netmiko</strong>, <strong>paramiko</strong>, or <strong>scrapli</strong> modules. They let you emulate the standard CLI: sending some text commands to the session and expecting back the text output of the more or less predictable readability level and formatting.<br />
There are also several high-level Python frameworks allowing for additional features on top of the tools I mentioned above. The two most useful of them in our case are <a href="https://napalm.readthedocs.io/en/latest/">NAPALM</a> and <a href="https://nornir.readthedocs.io/en/latest/">Nornir</a>. NAPALM provides vendor-neutral <a href="https://napalm.readthedocs.io/en/latest/support/">GETTER</a>s for getting structured data from the network devices. Nornir implements many useful abstractions and multithreading out of the box.<br />
As for SNMP, let’s leave it for network monitoring purposes.</p>
  </li>
  <li>
    <p><strong>Unstructured Data -&gt; Data Normalization Toolset -&gt; Structured Data</strong><br />
Data gathering with API usually allows you to get structured output straight 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 <strong>re</strong> module and regular expressions. Modern approaches are <a href="https://pyneng.readthedocs.io/ru/latest/book/22_textfsm/">TextFSM</a> framework developed by Google and brand new <a href="https://ttp.readthedocs.io/en/latest/">TTP (Template Text Parser)</a> developed by <a href="https://github.com/dmulyalin/">dmulyalin</a>. Both tools perform data parsing with more usable templates in comparison to regular expressions.<br />
NAPALM module mentioned above performs unstructured data normalization internally for supported GETTERs and returns the structured output. This may make things some easier in our case.</p>
  </li>
  <li>
    <p><strong>Data Processing and Analysis -&gt; Topology Representation in Python Datastructures</strong><br />
Once we get the structured topology data pieces from all our devices, all we need to do is to bring it to common representation, analyze and assemble a final puzzle.</p>
  </li>
  <li>
    <p><strong>Topology Representation in Visualization Engine Format</strong><br />
Depending on visualization engine selection, you may need to transform a final topology data format according to what the tool supports as an input.</p>
  </li>
  <li>
    <p><strong>Visualization Engine</strong><br />
This point was the most not obvious 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. Then I found JavaScript+HTML5 <a href="https://developer.cisco.com/site/neXt/">NeXt UI</a> Toolkit I once bookmarked while digging through Cisco DevNet labs before. This is a specialized network visualization toolkit developed by Cisco. It has many features and decent <a href="https://github.com/NeXt-UI/next-tutorials">documentation</a>.</p>
  </li>
  <li>
    <p><strong>Visualized Topology</strong><br />
Our final goal. A view may vary from a simple static image or an HTML document to something more advanced and interactive.</p>
  </li>
</ol>

<p>Here is a summary of the most common tools we have got:
<img src="https://habrastorage.org/webt/g4/at/4r/g4at4rurmjyp5sp2s0gwijdbbji.png" alt="detailed" />
<br /></p>

<p>Based on the requirements above I have selected the following tools for my target solution:</p>
<ul>
  <li>LLDP is a topology data source.</li>
  <li>SSH and CLI for interaction with the network devices.</li>
  <li><a href="https://nornir.readthedocs.io/en/latest/">Nornir</a> for multithreading, more useful data gathering result handling and processing, and keeping the information about our devices in a structured Inventory.</li>
  <li><a href="https://napalm.readthedocs.io/en/latest/">NAPALM</a> to abstract from manual CLI scrapping.</li>
  <li>Python3 for writing the core logic.</li>
  <li><a href="https://developer.cisco.com/site/neXt/">NeXt UI</a> (JS+HTML5) for topology visualization based on result we get from Python peace of code.<br /></li>
</ul>

<p>I have already used NAPALM and Nornir successfully for network audits and data gathering from hundreds of network devices before. Default NAPALM GETTERs support LLDP on Cisco IOS/IOS-XE, IOS-XR, NX-OS, Juniper JunOS, and Arista EOS.<br />
Furthermore, the logic separation discussed above would allow us to add more data sources and network connectors without affecting the whole codebase.<br />
Next UI was a thing to familiarize with and figure out how it works on the run. However, the <a href="https://developer.cisco.com/site/neXt/discover/demo/">examples</a> looked promising.</p>

<h1 id="preparation">Preparation</h1>

<h3 id="test-lab">Test lab</h3>

<p>I used <a href="https://devnetsandbox.cisco.com/RM/Diagram/Index/685f774a-a5d6-4df5-a324-3774217d0e6b?diagramType=Topology">Cisco Modeling Labs</a> as a test 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 using AnyConnect VPN once you receive the reservation details back to your email). In the old days we would have to use <s>a production network</s> a bare metal homelab or to have fun with GNS3.<br /><br /></p>

<p>Lab topology on the CML web interface looks as follows (we should get the similar picture as result):
<img src="https://habrastorage.org/webt/x5/ib/hu/x5ibhuvg2oem-vjb-x3q0aq_66w.png" alt="" />
<br />
It consists of Cisco devices of any kind: IOS (edge-sw01), IOSXE (internet-rtr01, distr-rtr01, distr-rtr02), NXOS (dist-sw01, dist-sw02), IOSXR (core-rtr01, core-rtr02), ASA (edge-firewall01). LLDP is enabled on all of them. SSH access is available on IOS, IOSXE, and NXOS nodes.</p>

<h3 id="installing-and-initializing-nornir">Installing and initializing Nornir</h3>

<p>Nornir is an open-source Python framework. It is available on PyPI for Python 3.6.2 and above. Nornir has a dozen of dependencies, including NAPALM and netmiko. It is recommended to use Python virtual environments (<a href="https://docs.python.org/3/library/venv.html">venv</a> to isolate the dependencies. My local development environment used Nornir 2.4.0 with Python 3.7 on MacOS 10.15. This should work on Linux and Windows as well. Nornir installation is straightforward:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">mkdir</span> ~/testenv
<span class="nv">$ </span>python3.7 <span class="nt">-m</span> venv ~/testenv/
<span class="nv">$ </span><span class="nb">source</span> ~/testenv/bin/activate
<span class="o">(</span>testenv<span class="o">)</span><span class="nv">$ </span>pip <span class="nb">install </span><span class="nv">nornir</span><span class="o">==</span>2.4.0
</code></pre></div></div>
<p><br /></p>

<p><strong>Important:</strong>  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.</p>

<p>Nornir supports various <a href="https://nornir.readthedocs.io/en/stable/plugins/inventory/index.html">inventory</a> 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.<br />
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.<br /></p>

<p><b>nornir_config.yaml:</b></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">core</span><span class="pi">:</span>
    <span class="na">num_workers</span><span class="pi">:</span> <span class="m">20</span>
<span class="na">inventory</span><span class="pi">:</span>
    <span class="na">plugin</span><span class="pi">:</span> <span class="s">nornir.plugins.inventory.simple.SimpleInventory</span>
    <span class="na">options</span><span class="pi">:</span>
        <span class="na">host_file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inventory/hosts_devnet_sb_cml.yml"</span>
        <span class="na">group_file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inventory/groups.yml"</span>
</code></pre></div></div>

<p>A sample Nornir primary configuration file you can see above contains references to two more YAML files: hosts file and groups file. These files define the SimpleInventory plugin configuration. The Hosts file contains a list of our network devices (hosts) with their attributes. Groups file contains a list of groups and their attributes. An individual host can be included in one or more groups. The host inherits the attributes of all groups it belongs to. Hosts and groups file names and locations can be arbitrary as well.</p>

<p><br />
<b>inventory/hosts_devnet_sb_cml.yml</b> has the following structure:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>

<span class="na">internet-rtr01</span><span class="pi">:</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">10.10.20.181</span>
    <span class="na">platform</span><span class="pi">:</span> <span class="s">ios</span>
    <span class="na">groups</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">devnet-cml-lab</span>

<span class="na">dist-sw01</span><span class="pi">:</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">10.10.20.177</span>
    <span class="na">platform</span><span class="pi">:</span> <span class="s">nxos_ssh</span>
    <span class="na">transport</span><span class="pi">:</span> <span class="s">ssh</span>
    <span class="na">groups</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">devnet-cml-lab</span>
</code></pre></div></div>

<p><em>Just two hosts are being displayed for brevity.</em> Both hosts have IP-address, platform attributes. dist-sw01 has transport type assigned specifically. For internet-rtr01, transport type will be chosen based on platform type (it is SSH for IOS by default) by Nornir. Both hosts belong to the ‘devnet-cml-lab’ group.</p>

<p><b>groups.yml</b> will define all group settings for them:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>

<span class="na">devnet-cml-lab</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">cisco</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">cisco</span>
    <span class="na">connection_options</span><span class="pi">:</span>
        <span class="na">napalm</span><span class="pi">:</span>
            <span class="na">extras</span><span class="pi">:</span>
                <span class="na">optional_args</span><span class="pi">:</span>
                    <span class="na">secret</span><span class="pi">:</span> <span class="s">cisco</span>
</code></pre></div></div>

<p>Group attributes above contain access credentials and enable secret for Cisco devices. These attributes will be inherited by all group members.<br />
<strong>Important:</strong> Never store credentials (and any sensitive data) in your production environment in clear text configs like this. This simple config is used for demonstrational and lab purposes only.<br />
Those are all general Nornir configuration steps. All we need to do now is to initialize it from a Python code.</p>

<h3 id="downloading-next-ui">Downloading NeXt UI</h3>

<p>For local usage and testing, it is enough to download the NeXt UI source code from <a href="https://github.com/NeXt-UI/next-bower">GitHub</a>. Let’s put the sources into ./next_sources in our project root directory.</p>

<hr />

<p>Our progress upon download completion:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>tree <span class="nb">.</span> <span class="nt">-L</span> 2
<span class="nb">.</span>
├── inventory
│   ├── groups.yml
│   └── hosts_devnet_sb_cml.yml
├── next_sources
│   ├── css
│   ├── doc
│   ├── fonts
│   └── js
├── nornir_config.yml
</code></pre></div></div>

<h1 id="age-of-topology-discovery">Age of Topology Discovery</h1>

<p>The main logic will be written in a Python script named <strong>generate_topology.py</strong>.</p>

<h3 id="initializing-nornir">Initializing Nornir</h3>

<p>Once our Nornir config is ready, it can be initialized in Python as simply as:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">nornir</span> <span class="kn">import</span> <span class="n">InitNornir</span>
<span class="kn">from</span> <span class="nn">nornir.plugins.tasks.networking</span> <span class="kn">import</span> <span class="n">napalm_get</span>

<span class="n">NORNIR_CONFIG_FILE</span> <span class="o">=</span> <span class="s">"nornir_config.yml"</span>

<span class="n">nr</span> <span class="o">=</span> <span class="n">InitNornir</span><span class="p">(</span><span class="n">config_file</span><span class="o">=</span><span class="n">NORNIR_CONFIG_FILE</span><span class="p">)</span>
</code></pre></div></div>
<p>That’s it. Nornir is ready to work.
<em>napalm_get</em> imported above allows us to use NAPALM straight from Nornir.<br /></p>

<h3 id="lldp-at-a-glance">LLDP at-a-glance</h3>

<p>LLDP-enabled devices exchange periodic LLDP messages consisting of <a href="https://en.wikipedia.org/wiki/Type-length-value">TLV</a>-fields with their direct neighbors. LLDP messages are not normally relayed.<br />
Mandatory TLV fields: Chassis ID, Port ID, Time-to-Live.
Optional TLV fields: System Name and Description; Port Name and Description; VLAN Name; IP Management address; System Capabilities (switching, routing, etc.), and more.
As the examined topology segment is under our control, let’s consider System Name and Port Name TLV fields required and advertisable internally.<br />
It does not cause significant security risks but allows us to identify multi-chassis devices with a shared control plane (e.g. stacked switches) and device interconnections uniquely.
<br /><br />
In this case, a topology analysis task as a whole can be reduced to the analysis of the neighborship data received on each device. This allows us to identify unique devices and their interconnections (i.e. Vertices and Edges of the topology Graph).<br />
<em>By the way, OSPF LSA exchange and analysis work in a very similar way. Visualizing routing protocol data may also be a good use case (I’d recommend to check out the <a href="https://topolograph.com">Topolograph</a> service released in October 2020 by @<a href="https://github.com/Vadims06">Vadims06</a>). But let’s focus on LLDP for now.</em>
<br /><br /></p>

<p>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 have any LLDP neighbors.<br /></p>

<p>Here is a manual <em>“show lldp neighbors”</em> output from dist-rtr01:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dist-rtr01#show lldp neighbors
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
</code></pre></div></div>
<p>Five neighbors. Looks good.<br />
The same output from core-rtr02:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RP/0/0/CPU0:core-rtr02#show lldp neighbors
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
</code></pre></div></div>

<p>Four neighbors. That’s correct as well.<br />
Please note that the output contains incomplete hostnames in a Device ID column in both cases.<br />
CLI automation always comes along with such issues.<br />
In our given case the workaround is to use a detailed output format.<br />
As an example:</p>

<details>
<summary>"show lldp neighbors detail" from IOSXE-based dist-rtr01</summary>

<pre>
dist-rtr01#show lldp neighbors detail
------------------------------------------------
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
</pre>

</details>

<details>
<summary>"show lldp neighbors detail" from NXOS-based dist-sw01</summary>

<pre>
dist-sw01# show lldp neighbors detail
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
</pre>

</details>

<h3 id="gathering-the-data-from-the-devices">Gathering the data from the devices</h3>

<p>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).<br />
IOS-XR-based core routers (core-rtr01, core-rtr02) will be intentionally restricted from management access.<br />
Thereby the following scenarios will be covered:</p>
<ol>
  <li>Full mesh neighborship handling for all <em>distribution</em> layer devices.
All unique nodes and links should be discovered properly.</li>
  <li>Device access or connectivity issues handling for core-rtr01 and core-rtr02.
This should not affect the ability to work with the rest of the devices.</li>
  <li>Building the topology based on partial data from discontiguous network segments.
Both edge switch and distribution routers “see” core-rtr01 and core-rtr02 from different sides.<br />
This should be enough to build the full picture.</li>
</ol>

<details>
<summary>Full inventory/hosts_devnet_sb_cml.yml hosts file content</summary>

<pre>
---

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:
    # Device access is restricted for the test
    hostname: 10.10.20.173
    platform: iosxr
    groups:
        - devnet-cml-lab

core-rtr02:
    # Device 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-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

</pre>

</details>

<p>NAPALM GETTERs to use:</p>
<ul>
  <li>GET_LLDP_NEIGHBORS_DETAILS.<br />
A detailed output version is chosen as it provides more consistent data.</li>
  <li>GET_FACTS.<br />
It collects some extended device information such as FQDN, model, serial number, etc.<br /></li>
</ul>

<p>Let’s wrap the data gathering task into a Nornir Task function.<br />
This is one of useful method of grouping actions on the individual hosts.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_host_data</span><span class="p">(</span><span class="n">task</span><span class="p">):</span>
    <span class="s">"""Nornir Task for data collection on target hosts."""</span>
    <span class="n">task</span><span class="p">.</span><span class="n">run</span><span class="p">(</span>
        <span class="n">task</span><span class="o">=</span><span class="n">napalm_get</span><span class="p">,</span>
        <span class="n">getters</span><span class="o">=</span><span class="p">[</span><span class="s">'facts'</span><span class="p">,</span> <span class="s">'lldp_neighbors_detail'</span><span class="p">]</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Now we can run the Task and save the result into variable for a further processing.<br />
Default execution scope is all devices.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get_host_data_result</span> <span class="o">=</span> <span class="n">nr</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">get_host_data</span><span class="p">)</span>
</code></pre></div></div>

<p>You may also use <a href="https://nornir.readthedocs.io/en/latest/tutorials/intro/inventory.html#Filtering-the-inventory">simple</a> and <a href="https://nornir.readthedocs.io/en/latest/howto/advanced_filtering.html">complex</a> inventory filters to limit the execution scope to individual hosts or groups.</p>

<h3 id="processing-the-data-received-from-devices">Processing the data received from devices</h3>

<p>get_host_data_result variable contains a get_host_data task execution results for each target device.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;&gt;</span> <span class="n">get_host_data_result</span>
<span class="n">AggregatedResult</span> <span class="p">(</span><span class="n">get_host_data</span><span class="p">):</span> <span class="p">{</span><span class="s">'internet-rtr01'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'edge-sw01'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'core-rtr01'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'core-rtr02'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'dist-rtr01'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'dist-rtr02'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'dist-sw01'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">],</span> <span class="s">'dist-sw02'</span><span class="p">:</span> <span class="n">MultiResult</span><span class="p">:</span> <span class="p">[</span><span class="n">Result</span><span class="p">:</span> <span class="s">"get_host_data"</span><span class="p">,</span> <span class="n">Result</span><span class="p">:</span> <span class="s">"napalm_get"</span><span class="p">]}</span>
</code></pre></div></div>

<p>Every host result object has <em>failed</em> method returning a boolean value. False means no errors occured during task execution on a specific host.<br />
The global task result is iterable as a dictionalry object:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;&gt;</span> <span class="k">for</span> <span class="n">device</span><span class="p">,</span> <span class="n">result</span> <span class="ow">in</span> <span class="n">get_host_data_result</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
<span class="p">...</span>     <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">device</span><span class="si">}</span><span class="s"> failed: </span><span class="si">{</span><span class="n">result</span><span class="p">.</span><span class="n">failed</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
<span class="p">...</span> 
<span class="n">internet</span><span class="o">-</span><span class="n">rtr01</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
<span class="n">edge</span><span class="o">-</span><span class="n">sw01</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
<span class="n">core</span><span class="o">-</span><span class="n">rtr01</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">True</span>
<span class="n">core</span><span class="o">-</span><span class="n">rtr02</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">True</span>
<span class="n">dist</span><span class="o">-</span><span class="n">rtr01</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
<span class="n">dist</span><span class="o">-</span><span class="n">rtr02</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
<span class="n">dist</span><span class="o">-</span><span class="n">sw01</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
<span class="n">dist</span><span class="o">-</span><span class="n">sw02</span> <span class="n">failed</span><span class="p">:</span> <span class="bp">False</span>
</code></pre></div></div>
<p>Looks expectedly.<br /></p>

<p>Some complete result outputs for the reference:</p>
<details>
<summary>Result object content for dist-rtr01</summary>

<pre>
&gt;&gt;&gt; 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': ''}]}}
</pre>

</details>

<details>
<summary>Result object content for dist-sw01</summary>

<pre>
&gt;&gt;&gt; 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': ''}]}}
</pre>

</details>

<p>Result object is a dictionary with a keys matching selected GETTER names: ‘facts’ and ‘lldp_neighbors_detail’.<br />
Dictionary values contain a structured data processed and returned by NAPALM.<br />
Let’s compare a neighbor sets:</p>

<details>
<summary>LLDP neigbors of dist-rtr01</summary>

<pre>
&gt;&gt;&gt; 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': ''}])
</pre>

</details>

<details>
<summary>LLDP neigbors of dist-sw01</summary>

<pre>
&gt;&gt;&gt; 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': ''}])
</pre>

</details>

<p>Five neighbors for dist-rtr01 and four neighbors for dist-sw01 which is exactly what we saw in CLI outputs before.<br />
The rest of the data is also valid.<br /></p>

<p>For ease of processing let’s split LLDP and facts data into separate entities.<br />
Any devices may be also present in multiple outputs. To distinguish between them, it is necessary to use some unique node identifiers. Let’s choose it in the following descending order:</p>
<ul>
  <li>Device FQDN if available (may be further referred to as hostname for simplicity).</li>
  <li>Device hostname if available.</li>
  <li>Device host object name from Nornir Inventory.</li>
</ul>

<p>LLDP relies on the first two steps too.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">normalize_result</span><span class="p">(</span><span class="n">nornir_job_result</span><span class="p">):</span>
    <span class="s">"""
    get_host_data result parser.
    Returns LLDP and FACTS data dicts
    with hostname keys.
    """</span>
    <span class="n">global_lldp_data</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">global_facts</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">device</span><span class="p">,</span> <span class="n">output</span> <span class="ow">in</span> <span class="n">nornir_job_result</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">output</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">failed</span><span class="p">:</span>
            <span class="c1"># Write default data to dicts if the task is failed.
</span>            <span class="c1"># Use the host inventory object name as a key.
</span>            <span class="n">global_lldp_data</span><span class="p">[</span><span class="n">device</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
            <span class="n">global_facts</span><span class="p">[</span><span class="n">device</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
                <span class="s">'nr_ip'</span><span class="p">:</span> <span class="n">nr</span><span class="p">.</span><span class="n">inventory</span><span class="p">.</span><span class="n">hosts</span><span class="p">[</span><span class="n">device</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'hostname'</span><span class="p">,</span> <span class="s">'n/a'</span><span class="p">),</span>
            <span class="p">}</span>
            <span class="k">continue</span>
        <span class="c1"># Use FQDN as unique ID for devices withing the script.
</span>        <span class="n">device_fqdn</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">result</span><span class="p">[</span><span class="s">'facts'</span><span class="p">][</span><span class="s">'fqdn'</span><span class="p">]</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">device_fqdn</span><span class="p">:</span>
            <span class="c1"># If FQDN is not set use hostname.
</span>            <span class="c1"># LLDP TLV follows the same logic.
</span>            <span class="n">device_fqdn</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">result</span><span class="p">[</span><span class="s">'facts'</span><span class="p">][</span><span class="s">'hostname'</span><span class="p">]</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">device_fqdn</span><span class="p">:</span>
            <span class="c1"># Use host inventory object name as a key if
</span>            <span class="c1"># neither FQDN nor hostname are set
</span>            <span class="n">device_fqdn</span> <span class="o">=</span> <span class="n">device</span>
        <span class="n">global_facts</span><span class="p">[</span><span class="n">device_fqdn</span><span class="p">]</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">result</span><span class="p">[</span><span class="s">'facts'</span><span class="p">]</span>
        <span class="c1"># Populate device facts with its IP address or hostname as per Inventory data
</span>        <span class="n">global_facts</span><span class="p">[</span><span class="n">device_fqdn</span><span class="p">][</span><span class="s">'nr_ip'</span><span class="p">]</span> <span class="o">=</span> <span class="n">nr</span><span class="p">.</span><span class="n">inventory</span><span class="p">.</span><span class="n">hosts</span><span class="p">[</span><span class="n">device</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'hostname'</span><span class="p">,</span> <span class="s">'n/a'</span><span class="p">)</span>
        <span class="n">global_lldp_data</span><span class="p">[</span><span class="n">device_fqdn</span><span class="p">]</span> <span class="o">=</span> <span class="n">output</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">result</span><span class="p">[</span><span class="s">'lldp_neighbors_detail'</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">global_lldp_data</span><span class="p">,</span> <span class="n">global_facts</span>
</code></pre></div></div>

<p>Then we should extract a list of all neighbors known by devices and build based on that:</p>
<ul>
  <li>A list of unique hosts.</li>
  <li>A list of unique links between them.</li>
</ul>

<p>To ensure unambiguous identification of links, we will store them in a following format:<br />
((source_device_id, source_port_name), (destination_device_id, destination_port_name))<br /></p>

<p>It is also necessary to keep in mind that:</p>
<ul>
  <li>A link may be visible from two sides if we collect the data from both devices it is connected to.<br />
We have to check for side A and side B permutations to filter out the duplicates.</li>
  <li>A port name may be formatted differently in the LLDP announcement and local outputs. For example, <em>GigabitEthernet4</em> locally and <em>Gi4</em> in LLDP Port name.</li>
</ul>

<p>To ensure data consistency, we will translate the port names into a full format for the analysis stage. At the same time, let’s implement a name shortening function to provide better visual experience during visualization.<br />
Automatic icon selection can be implemented based on device capabilities advertised in LLDP. Let’s extract them into a separate {“hostname”: “primary_capability”} dictionary.<br />
The same code wise:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">interface_full_name_map</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'Eth'</span><span class="p">:</span> <span class="s">'Ethernet'</span><span class="p">,</span>
    <span class="s">'Fa'</span><span class="p">:</span> <span class="s">'FastEthernet'</span><span class="p">,</span>
    <span class="s">'Gi'</span><span class="p">:</span> <span class="s">'GigabitEthernet'</span><span class="p">,</span>
    <span class="s">'Te'</span><span class="p">:</span> <span class="s">'TenGigabitEthernet'</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">def</span> <span class="nf">if_fullname</span><span class="p">(</span><span class="n">ifname</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">interface_full_name_map</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">ifname</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">v</span><span class="p">):</span>
            <span class="k">return</span> <span class="n">ifname</span>
        <span class="k">if</span> <span class="n">ifname</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">k</span><span class="p">):</span>
            <span class="k">return</span> <span class="n">ifname</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">ifname</span>

<span class="k">def</span> <span class="nf">if_shortname</span><span class="p">(</span><span class="n">ifname</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">interface_full_name_map</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">ifname</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">v</span><span class="p">):</span>
            <span class="k">return</span> <span class="n">ifname</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">ifname</span>


<span class="k">def</span> <span class="nf">extract_lldp_details</span><span class="p">(</span><span class="n">lldp_data_dict</span><span class="p">):</span>
    <span class="s">"""
    LLDP data dict parser.
    Returns set of all the discovered hosts,
    LLDP capabilities dict with all LLDP-discovered host,
    and all discovered interconnections between hosts.
    """</span>
    <span class="n">discovered_hosts</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="n">lldp_capabilities_dict</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">global_interconnections</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">host</span><span class="p">,</span> <span class="n">lldp_data</span> <span class="ow">in</span> <span class="n">lldp_data_dict</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">host</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">discovered_hosts</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">host</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">lldp_data</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">for</span> <span class="n">interface</span><span class="p">,</span> <span class="n">neighbors</span> <span class="ow">in</span> <span class="n">lldp_data</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">for</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">neighbors</span><span class="p">:</span>
                <span class="k">if</span> <span class="ow">not</span> <span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">]:</span>
                    <span class="k">continue</span>
                <span class="n">discovered_hosts</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">])</span>
                <span class="k">if</span> <span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_enable_capab'</span><span class="p">]:</span>
                    <span class="c1"># In case of multiple enable capabilities pick first in the list
</span>                    <span class="n">lldp_capabilities_dict</span><span class="p">[</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">]]</span> <span class="o">=</span> <span class="p">(</span>
                        <span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_enable_capab'</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
                    <span class="p">)</span>
                <span class="k">else</span><span class="p">:</span>
                    <span class="n">lldp_capabilities_dict</span><span class="p">[</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">]]</span> <span class="o">=</span> <span class="s">''</span>
                <span class="c1"># Store interconnections in a following format:
</span>                <span class="c1"># ((source_hostname, source_port), (dest_hostname, dest_port))
</span>                <span class="n">local_end</span> <span class="o">=</span> <span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">interface</span><span class="p">)</span>
                <span class="n">remote_end</span> <span class="o">=</span> <span class="p">(</span>
                    <span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">],</span>
                    <span class="n">if_fullname</span><span class="p">(</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_port'</span><span class="p">])</span>
                <span class="p">)</span>
                <span class="c1"># Check if the link is not a permutation of already added one
</span>                <span class="c1"># (local_end, remote_end) equals (remote_end, local_end)
</span>                <span class="n">link_is_already_there</span> <span class="o">=</span> <span class="p">(</span>
                    <span class="p">(</span><span class="n">local_end</span><span class="p">,</span> <span class="n">remote_end</span><span class="p">)</span> <span class="ow">in</span> <span class="n">global_interconnections</span>
                    <span class="ow">or</span> <span class="p">(</span><span class="n">remote_end</span><span class="p">,</span> <span class="n">local_end</span><span class="p">)</span> <span class="ow">in</span> <span class="n">global_interconnections</span>
                <span class="p">)</span>
                <span class="k">if</span> <span class="n">link_is_already_there</span><span class="p">:</span>
                    <span class="k">continue</span>
                <span class="n">global_interconnections</span><span class="p">.</span><span class="n">append</span><span class="p">((</span>
                    <span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">interface</span><span class="p">),</span>
                    <span class="p">(</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_system_name'</span><span class="p">],</span> <span class="n">if_fullname</span><span class="p">(</span><span class="n">neighbor</span><span class="p">[</span><span class="s">'remote_port'</span><span class="p">]))</span>
                <span class="p">))</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">discovered_hosts</span><span class="p">,</span> <span class="n">global_interconnections</span><span class="p">,</span> <span class="n">lldp_capabilities_dict</span><span class="p">]</span>
</code></pre></div></div>

<h3 id="initializing-next-ui-application">Initializing NeXt UI application</h3>

<p>Topology visualization logic will be implemented in <em>next_app.js</em> script based on Next UI.<br />
Let’s start with the basics:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">nx</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/**
     * NeXt UI based application
     */</span>
    <span class="c1">// Initialize topology</span>
    <span class="kd">var</span> <span class="nx">topo</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">nx</span><span class="p">.</span><span class="nx">graphic</span><span class="p">.</span><span class="nx">Topology</span><span class="p">({</span>
        <span class="c1">// View dimensions</span>
        <span class="na">width</span><span class="p">:</span> <span class="mi">1200</span><span class="p">,</span>
        <span class="na">height</span><span class="p">:</span> <span class="mi">700</span><span class="p">,</span>
        <span class="c1">// Dataprocessor is responsible for spreading </span>
        <span class="c1">// the Nodes across the view.</span>
        <span class="c1">// 'force' data processor spreads the Nodes so</span>
        <span class="c1">// they would be as distant from each other</span>
        <span class="c1">// as possible. Follow social distancing and stay healthy.</span>
        <span class="c1">// 'quick' dataprocessor picks random positions</span>
        <span class="c1">// for the Nodes.</span>
        <span class="na">dataProcessor</span><span class="p">:</span> <span class="dl">'</span><span class="s1">force</span><span class="dl">'</span><span class="p">,</span>
        <span class="c1">// Node and Link identity key attribute name</span>
        <span class="na">identityKey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span>
        <span class="c1">// Node settings</span>
        <span class="na">nodeConfig</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">label</span><span class="p">:</span> <span class="dl">'</span><span class="s1">model.name</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">iconType</span><span class="p">:</span><span class="dl">'</span><span class="s1">model.icon</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="c1">// Link settings</span>
        <span class="na">linkConfig</span><span class="p">:</span> <span class="p">{</span>
            <span class="c1">// Display Links as curves in case of </span>
            <span class="c1">// multiple links between Node Pairs.</span>
            <span class="c1">// Set to 'parallel' to use parallel links.</span>
            <span class="na">linkType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">curve</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="c1">// Display Node icon. Displays a dot if set to 'false'.</span>
        <span class="na">showIcon</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="kd">var</span> <span class="nx">Shell</span> <span class="o">=</span> <span class="nx">nx</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="nx">nx</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">Application</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">methods</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">start</span><span class="p">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
                <span class="c1">// Read topology data from variable</span>
                <span class="nx">topo</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="nx">topologyData</span><span class="p">);</span>
                <span class="c1">// Attach it to the document</span>
                <span class="nx">topo</span><span class="p">.</span><span class="nx">attach</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">});</span>

    <span class="c1">// Create an application instance</span>
    <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Shell</span><span class="p">();</span>
    <span class="c1">// Run the application</span>
    <span class="nx">shell</span><span class="p">.</span><span class="nx">start</span><span class="p">();</span>
<span class="p">})(</span><span class="nx">nx</span><span class="p">);</span>
</code></pre></div></div>

<p>The topology data structure will be stored in a <em>topologyData</em> variable. Let’s move it into a separate <em>topology.js</em> file. The format details will be discussed below.<br /></p>

<p>A final visualization result will be displayed in a local HTML form with attached JS components:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>

<span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"next_sources/css/next.css"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"styles_main_page.css"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"next_sources/js/next.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"topology.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"next_app.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<h3 id="generating-next-ui-topology-in-python">Generating NeXt UI topology in Python</h3>

<p>We have already written required data gathering result handlers and normalized it using Python data structures.<br />
Let’s apply this:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">GLOBAL_LLDP_DATA</span><span class="p">,</span> <span class="n">GLOBAL_FACTS</span> <span class="o">=</span> <span class="n">normalize_result</span><span class="p">(</span><span class="n">get_host_data_result</span><span class="p">)</span>
<span class="n">TOPOLOGY_DETAILS</span> <span class="o">=</span> <span class="n">extract_lldp_details</span><span class="p">(</span><span class="n">GLOBAL_LLDP_DATA</span><span class="p">)</span>
</code></pre></div></div>
<p>General NeXt UI topology representation looks as follows:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Two nodes connected with two links</span>
<span class="kd">var</span> <span class="nx">topologyData</span> <span class="o">=</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">links</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">source</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
        <span class="p">},</span> <span class="p">{</span>
            <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">source</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="p">],</span>
    <span class="dl">"</span><span class="s2">nodes</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="dl">"</span><span class="s2">icon</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">router</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">{</span>
            <span class="dl">"</span><span class="s2">icon</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">router</span><span class="dl">"</span><span class="p">,</span>
            <span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="p">]</span>
</code></pre></div></div>

<p>As you can see, this is a JSON object which can be mapped to a Python data structure of the following format: {‘nodes’: [], ‘links’: []}. <br />
We will bring all our data together into that.<br />
In addition, let’s take device model into account for node icon selection to handle missing LLDP capabilities case.<br />
Node objects will also be populated with some extended attributes derived from GET_FACTS (model, S/N, etc) to enrich the topology view.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">icon_capability_map</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'router'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'switch'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'bridge'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'station'</span><span class="p">:</span> <span class="s">'host'</span>
<span class="p">}</span>

<span class="n">icon_model_map</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'CSR1000V'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'Nexus'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'IOSXRv'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'IOSv'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'2901'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'2911'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'2921'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'2951'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4321'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4331'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4351'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4421'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4431'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'4451'</span><span class="p">:</span> <span class="s">'router'</span><span class="p">,</span>
    <span class="s">'2960'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'3750'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
    <span class="s">'3850'</span><span class="p">:</span> <span class="s">'switch'</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">def</span> <span class="nf">get_icon_type</span><span class="p">(</span><span class="n">device_cap_name</span><span class="p">,</span> <span class="n">device_model</span><span class="o">=</span><span class="s">''</span><span class="p">):</span>
    <span class="s">"""
    Device icon selection function. Selection order:
    - LLDP capabilities mapping.
    - Device model mapping.
    - Default 'unknown'.
    """</span>
    <span class="k">if</span> <span class="n">device_cap_name</span><span class="p">:</span>
        <span class="n">icon_type</span> <span class="o">=</span> <span class="n">icon_capability_map</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">device_cap_name</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">icon_type</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">icon_type</span>
    <span class="k">if</span> <span class="n">device_model</span><span class="p">:</span>
        <span class="c1"># Check substring presence in icon_model_map keys
</span>        <span class="c1"># string until the first match
</span>        <span class="k">for</span> <span class="n">model_shortname</span><span class="p">,</span> <span class="n">icon_type</span> <span class="ow">in</span> <span class="n">icon_model_map</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
            <span class="k">if</span> <span class="n">model_shortname</span> <span class="ow">in</span> <span class="n">device_model</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">icon_type</span>
    <span class="k">return</span> <span class="s">'unknown'</span>

<span class="k">def</span> <span class="nf">generate_topology_json</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">):</span>
    <span class="s">"""
    JSON topology object generator.
    Takes as an input:
    - discovered hosts set,
    - LLDP capabilities dict with hostname keys,
    - interconnections list,
    - facts dict with hostname keys.
    """</span>
    <span class="n">discovered_hosts</span><span class="p">,</span> <span class="n">interconnections</span><span class="p">,</span> <span class="n">lldp_capabilities_dict</span><span class="p">,</span> <span class="n">facts</span> <span class="o">=</span> <span class="n">args</span>
    <span class="n">host_id</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">host_id_map</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="n">topology_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">'nodes'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'links'</span><span class="p">:</span> <span class="p">[]}</span>
    <span class="k">for</span> <span class="n">host</span> <span class="ow">in</span> <span class="n">discovered_hosts</span><span class="p">:</span>
        <span class="n">device_model</span> <span class="o">=</span> <span class="s">'n/a'</span>
        <span class="n">device_serial</span> <span class="o">=</span> <span class="s">'n/a'</span>
        <span class="n">device_ip</span> <span class="o">=</span> <span class="s">'n/a'</span>
        <span class="k">if</span> <span class="n">facts</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">host</span><span class="p">):</span>
            <span class="n">device_model</span> <span class="o">=</span> <span class="n">facts</span><span class="p">[</span><span class="n">host</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'model'</span><span class="p">,</span> <span class="s">'n/a'</span><span class="p">)</span>
            <span class="n">device_serial</span> <span class="o">=</span> <span class="n">facts</span><span class="p">[</span><span class="n">host</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'serial_number'</span><span class="p">,</span> <span class="s">'n/a'</span><span class="p">)</span>
            <span class="n">device_ip</span> <span class="o">=</span> <span class="n">facts</span><span class="p">[</span><span class="n">host</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'nr_ip'</span><span class="p">,</span> <span class="s">'n/a'</span><span class="p">)</span>
        <span class="n">host_id_map</span><span class="p">[</span><span class="n">host</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id</span>
        <span class="n">topology_dict</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">].</span><span class="n">append</span><span class="p">({</span>
            <span class="s">'id'</span><span class="p">:</span> <span class="n">host_id</span><span class="p">,</span>
            <span class="s">'name'</span><span class="p">:</span> <span class="n">host</span><span class="p">,</span>
            <span class="s">'primaryIP'</span><span class="p">:</span> <span class="n">device_ip</span><span class="p">,</span>
            <span class="s">'model'</span><span class="p">:</span> <span class="n">device_model</span><span class="p">,</span>
            <span class="s">'serial_number'</span><span class="p">:</span> <span class="n">device_serial</span><span class="p">,</span>
            <span class="s">'icon'</span><span class="p">:</span> <span class="n">get_icon_type</span><span class="p">(</span>
                <span class="n">lldp_capabilities_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="s">''</span><span class="p">),</span>
                <span class="n">device_model</span>
            <span class="p">)</span>
        <span class="p">})</span>
        <span class="n">host_id</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="n">link_id</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">link</span> <span class="ow">in</span> <span class="n">interconnections</span><span class="p">:</span>
        <span class="n">topology_dict</span><span class="p">[</span><span class="s">'links'</span><span class="p">].</span><span class="n">append</span><span class="p">({</span>
            <span class="s">'id'</span><span class="p">:</span> <span class="n">link_id</span><span class="p">,</span>
            <span class="s">'source'</span><span class="p">:</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">link</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]],</span>
            <span class="s">'target'</span><span class="p">:</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">link</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="mi">0</span><span class="p">]],</span>
            <span class="s">'srcIfName'</span><span class="p">:</span> <span class="n">if_shortname</span><span class="p">(</span><span class="n">link</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">1</span><span class="p">]),</span>
            <span class="s">'srcDevice'</span><span class="p">:</span> <span class="n">link</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span>
            <span class="s">'tgtIfName'</span><span class="p">:</span> <span class="n">if_shortname</span><span class="p">(</span><span class="n">link</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="mi">1</span><span class="p">]),</span>
            <span class="s">'tgtDevice'</span><span class="p">:</span> <span class="n">link</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span>
        <span class="p">})</span>
        <span class="n">link_id</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">return</span> <span class="n">topology_dict</span>
</code></pre></div></div>

<p>Then we should just write this Python topology dictionary into the <em>topology.js</em> file. A standard ** json ** module will work for this perfectly providing a readable and formatted output:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span>

<span class="n">OUTPUT_TOPOLOGY_FILENAME</span> <span class="o">=</span> <span class="s">'topology.js'</span>
<span class="n">TOPOLOGY_FILE_HEAD</span> <span class="o">=</span> <span class="s">"</span><span class="se">\n\n</span><span class="s">var topologyData = "</span>

<span class="k">def</span> <span class="nf">write_topology_file</span><span class="p">(</span><span class="n">topology_json</span><span class="p">,</span> <span class="n">header</span><span class="o">=</span><span class="n">TOPOLOGY_FILE_HEAD</span><span class="p">,</span> <span class="n">dst</span><span class="o">=</span><span class="n">OUTPUT_TOPOLOGY_FILENAME</span><span class="p">):</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="n">topology_file</span><span class="p">:</span>
        <span class="n">topology_file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">header</span><span class="p">)</span>
        <span class="n">topology_file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">topology_json</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>
        <span class="n">topology_file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">';'</span><span class="p">)</span>

<span class="n">TOPOLOGY_DICT</span> <span class="o">=</span> <span class="n">generate_topology_json</span><span class="p">(</span><span class="o">*</span><span class="n">TOPOLOGY_DETAILS</span><span class="p">)</span>
<span class="n">write_topology_file</span><span class="p">(</span><span class="n">TOPOLOGY_DICT</span><span class="p">)</span>
</code></pre></div></div>

<details>
<summary>Resulting topology.js file content</summary>

<pre>

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",
        }
    ]
};
</pre>

</details>

<p>Now let’s finally run <strong>main.html</strong> and see our visualization Hello World:<br />
<img src="https://habrastorage.org/webt/h_/bo/k-/h_bok-ejmfmzhxj9ppjhrjub2hk.png" alt="" />
Looks correct. All known nodes and links between them are displayed.<br />
The nodes are draggable in any direction. On mouse click on nodes and links, a Next UI tooltip menu appears. It contains all the attributes we previously passed into node and link topology objects in Python:<br />
<img src="https://habrastorage.org/webt/d8/hn/j1/d8hnj1fe0u5xduawmcw5ce2t720.png" alt="" />
<br />
Not so bad. There is also room for improvement. We will return to this some later on. Let’s implement a solution for the second part of the task for now.</p>

<h1 id="detecting-and-visualizing-topology-changes">Detecting and visualizing topology changes</h1>

<p>A bonus task is to detect and visualize topology changes.<br />
To get it done, some additions will be required:</p>
<ul>
  <li>A topology cache file <strong>cached_topology.js</strong> to store a previous topology state.
<strong>generate_topology.py</strong> script will be reading this cache file on each run and rewriting with a newer state if necessary.</li>
  <li><strong>diff_topology.js</strong> topology file to store a diff topology.</li>
  <li><strong>diff_page.html</strong> page to display a visualized topology diff.</li>
</ul>

<p>Diff HTML-form will look as follows:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>

<span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"next_sources/css/next.css"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"styles_main_page.css"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"next_sources/js/next.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"diff_topology.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"next_app.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
    <span class="nt">&lt;/head&gt;</span>
    <span class="nt">&lt;body&gt;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"main.html"</span><span class="nt">&gt;&lt;button&gt;</span>Display current topology<span class="nt">&lt;/button&gt;&lt;/a&gt;</span>
        <span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>All we need to read and write topology cache files:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CACHED_TOPOLOGY_FILENAME</span> <span class="o">=</span> <span class="s">'cached_topology.json'</span>

<span class="k">def</span> <span class="nf">write_topology_cache</span><span class="p">(</span><span class="n">topology_json</span><span class="p">,</span> <span class="n">dst</span><span class="o">=</span><span class="n">CACHED_TOPOLOGY_FILENAME</span><span class="p">):</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span> <span class="k">as</span> <span class="n">cached_file</span><span class="p">:</span>
        <span class="n">cached_file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">topology_json</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">read_cached_topology</span><span class="p">(</span><span class="n">filename</span><span class="o">=</span><span class="n">CACHED_TOPOLOGY_FILENAME</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">{}</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">{}</span>
    <span class="n">cached_topology</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="nb">file</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">cached_topology</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="nb">file</span><span class="p">.</span><span class="n">read</span><span class="p">())</span>
        <span class="k">except</span><span class="p">:</span>
            <span class="k">return</span> <span class="p">{}</span>
    <span class="k">return</span> <span class="n">cached_topology</span>
</code></pre></div></div>

<p>Topology diff analysis steps:</p>

<ol>
  <li>
    <p>Extract the node and link attributes for comparison from the current and cached topology dictionaries.
<br />
Node format:<br />
(node object with all attributes, (hostname,))<br />
Link format:<br />
(link object with all attributes, (source_hostnme, source_port), (dest_hostname, dest_port))<br />
Both node and link format allows further extension.</p>
  </li>
  <li>Compare the extracted node and link objects. Link format permutations should be considered.
<br />
Diff result for nodes and links will be written into two dictionaries in the following format:
    <ul>
      <li>diff_nodes = {‘added’: [], ‘deleted’: []}</li>
      <li>diff_links = {‘added’: [], ‘deleted’: []}</li>
    </ul>
  </li>
  <li>Merge current and cached topology with the diff data.<br />
The resulting topology will be written to diff_merged_topology dictionary.<br />
Deleted node and link objects will be extended with the <em>is_dead</em> attribute. For a better visual experience, deleted node icons will be customized (Next UI changes for that will be discussed below).<br />
New node and link objects will be extended with the <em>is_new</em> attribute.<br /></li>
</ol>

<p>Let’s code:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_topology_diff</span><span class="p">(</span><span class="n">cached</span><span class="p">,</span> <span class="n">current</span><span class="p">):</span>
    <span class="s">"""
    Topology diff analyzer and generator.
    Accepts two valid topology dicts as an input.
    Returns:
    - dict with added and deleted nodes,
    - dict with added and deleted links,
    - dict with merged input topologies with extended
      attributes for topology changes visualization
    """</span>
    <span class="n">diff_nodes</span> <span class="o">=</span> <span class="p">{</span><span class="s">'added'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'deleted'</span><span class="p">:</span> <span class="p">[]}</span>
    <span class="n">diff_links</span> <span class="o">=</span> <span class="p">{</span><span class="s">'added'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'deleted'</span><span class="p">:</span> <span class="p">[]}</span>
    <span class="n">diff_merged_topology</span> <span class="o">=</span> <span class="p">{</span><span class="s">'nodes'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'links'</span><span class="p">:</span> <span class="p">[]}</span>
    <span class="c1"># Parse links from topology dicts into the following format:
</span>    <span class="c1"># (topology_link_obj, (source_hostnme, source_port), (dest_hostname, dest_port))
</span>    <span class="n">cached_links</span> <span class="o">=</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="p">((</span><span class="n">x</span><span class="p">[</span><span class="s">'srcDevice'</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="s">'srcIfName'</span><span class="p">]),</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="s">'tgtDevice'</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="s">'tgtIfName'</span><span class="p">])))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cached</span><span class="p">[</span><span class="s">'links'</span><span class="p">]]</span>
    <span class="n">links</span> <span class="o">=</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="p">((</span><span class="n">x</span><span class="p">[</span><span class="s">'srcDevice'</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="s">'srcIfName'</span><span class="p">]),</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="s">'tgtDevice'</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="s">'tgtIfName'</span><span class="p">])))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">current</span><span class="p">[</span><span class="s">'links'</span><span class="p">]]</span>
    <span class="c1"># Parse nodes from topology dicts into the following format:
</span>    <span class="c1"># (topology_node_obj, (hostname,))
</span>    <span class="c1"># Some additional values might be added for comparison later on to the tuple above.
</span>    <span class="n">cached_nodes</span> <span class="o">=</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="s">'name'</span><span class="p">],))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cached</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">]]</span>
    <span class="n">nodes</span> <span class="o">=</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="s">'name'</span><span class="p">],))</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">current</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">]]</span>
    <span class="c1"># Search for deleted and added hostnames.
</span>    <span class="n">node_id</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">host_id_map</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">for</span> <span class="n">raw_data</span><span class="p">,</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">nodes</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">node</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cached_nodes</span><span class="p">]:</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">node_id</span>
            <span class="n">host_id_map</span><span class="p">[</span><span class="n">raw_data</span><span class="p">[</span><span class="s">'name'</span><span class="p">]]</span> <span class="o">=</span> <span class="n">node_id</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
            <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
            <span class="n">node_id</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="k">continue</span>
        <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'added'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">node_id</span>
        <span class="n">host_id_map</span><span class="p">[</span><span class="n">raw_data</span><span class="p">[</span><span class="s">'name'</span><span class="p">]]</span> <span class="o">=</span> <span class="n">node_id</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'yes'</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
        <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
        <span class="n">node_id</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">for</span> <span class="n">raw_data</span><span class="p">,</span> <span class="n">cached_node</span> <span class="ow">in</span> <span class="n">cached_nodes</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">cached_node</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">nodes</span><span class="p">]:</span>
            <span class="k">continue</span>
        <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">cached_node</span><span class="p">)</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">node_id</span>
        <span class="n">host_id_map</span><span class="p">[</span><span class="n">raw_data</span><span class="p">[</span><span class="s">'name'</span><span class="p">]]</span> <span class="o">=</span> <span class="n">node_id</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'yes'</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'icon'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'dead_node'</span>
        <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
        <span class="n">node_id</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="c1"># Search for deleted and added interconnections.
</span>    <span class="c1"># Interface change on some side is considered as
</span>    <span class="c1"># one interconnection deletion and one interconnection insertion.
</span>    <span class="c1"># Check for permutations as well:
</span>    <span class="c1"># ((h1, Gi1), (h2, Gi2)) and ((h2, Gi2), (h1, Gi1)) are equal.
</span>    <span class="n">link_id</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">raw_data</span><span class="p">,</span> <span class="n">link</span> <span class="ow">in</span> <span class="n">links</span><span class="p">:</span>
        <span class="n">src</span><span class="p">,</span> <span class="n">dst</span> <span class="o">=</span> <span class="n">link</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cached_links</span><span class="p">]</span> <span class="ow">and</span> <span class="ow">not</span> <span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">cached_links</span><span class="p">]:</span>
            <span class="n">diff_links</span><span class="p">[</span><span class="s">'added'</span><span class="p">].</span><span class="n">append</span><span class="p">((</span><span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">))</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">link_id</span>
            <span class="n">link_id</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'source'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">src</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'target'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">dst</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'yes'</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
            <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'links'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
            <span class="k">continue</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">link_id</span>
        <span class="n">link_id</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'source'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">src</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'target'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">dst</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
        <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
        <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'links'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">raw_data</span><span class="p">,</span> <span class="n">link</span> <span class="ow">in</span> <span class="n">cached_links</span><span class="p">:</span>
        <span class="n">src</span><span class="p">,</span> <span class="n">dst</span> <span class="o">=</span> <span class="n">link</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">links</span><span class="p">]</span> <span class="ow">and</span> <span class="ow">not</span> <span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span> <span class="ow">in</span> <span class="p">[</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">links</span><span class="p">]:</span>
            <span class="n">diff_links</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">].</span><span class="n">append</span><span class="p">((</span><span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">))</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">link_id</span>
            <span class="n">link_id</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'source'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">src</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'target'</span><span class="p">]</span> <span class="o">=</span> <span class="n">host_id_map</span><span class="p">[</span><span class="n">dst</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_new'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'no'</span>
            <span class="n">raw_data</span><span class="p">[</span><span class="s">'is_dead'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'yes'</span>
            <span class="n">diff_merged_topology</span><span class="p">[</span><span class="s">'links'</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">raw_data</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">diff_nodes</span><span class="p">,</span> <span class="n">diff_links</span><span class="p">,</span> <span class="n">diff_merged_topology</span>
</code></pre></div></div>
<p>get_topology_diff implements comparison of two arbitrary topology dictionaries of a valid format<br />
This allows us to implement a topology cache versioning in the future.<br />
Let’s also implement a console diff print function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">print_diff</span><span class="p">(</span><span class="n">diff_result</span><span class="p">):</span>
    <span class="s">"""
    Formatted get_topology_diff result
    console print function.
    """</span>
    <span class="n">diff_nodes</span><span class="p">,</span> <span class="n">diff_links</span><span class="p">,</span> <span class="o">*</span><span class="n">ignore</span> <span class="o">=</span> <span class="n">diff_result</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">diff_nodes</span><span class="p">[</span><span class="s">'added'</span><span class="p">]</span> <span class="ow">or</span> <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]</span> <span class="ow">or</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'added'</span><span class="p">]</span> <span class="ow">or</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]):</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'No topology changes since last run.'</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Topology changes have been discovered:'</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'added'</span><span class="p">]:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'^^^^^^^^^^^^^^^^^^^^'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'New Network Devices:'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'vvvvvvvvvvvvvvvvvvvv'</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'added'</span><span class="p">]:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Hostname: </span><span class="si">{</span><span class="n">node</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'^^^^^^^^^^^^^^^^^^^^^^^^'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'Deleted Network Devices:'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'vvvvvvvvvvvvvvvvvvvvvvvv'</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">diff_nodes</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Hostname: </span><span class="si">{</span><span class="n">node</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'added'</span><span class="p">]:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'^^^^^^^^^^^^^^^^^^^^^^'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'New Interconnections:'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'vvvvvvvvvvvvvvvvvvvvvv'</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span> <span class="ow">in</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'added'</span><span class="p">]:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'From </span><span class="si">{</span><span class="n">src</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">(</span><span class="si">{</span><span class="n">src</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">) To </span><span class="si">{</span><span class="n">dst</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">(</span><span class="si">{</span><span class="n">dst</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)'</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'^^^^^^^^^^^^^^^^^^^^^^^^^'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'Deleted Interconnections:'</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="s">'vvvvvvvvvvvvvvvvvvvvvvvvv'</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span> <span class="ow">in</span> <span class="n">diff_links</span><span class="p">[</span><span class="s">'deleted'</span><span class="p">]:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'From </span><span class="si">{</span><span class="n">src</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">(</span><span class="si">{</span><span class="n">src</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">) To </span><span class="si">{</span><span class="n">dst</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s">(</span><span class="si">{</span><span class="n">dst</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s">)'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
</code></pre></div></div>

<p>Finally, let’s summarize the code pieces we have written above into a dedicated main() function.<br />
Here are a fairly self-documenting code and my personal answer to a question “why not Ansible”:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">good_luck_have_fun</span><span class="p">():</span>
    <span class="s">"""Main script logic"""</span>
    <span class="n">get_host_data_result</span> <span class="o">=</span> <span class="n">nr</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">get_host_data</span><span class="p">)</span>
    <span class="n">GLOBAL_LLDP_DATA</span><span class="p">,</span> <span class="n">GLOBAL_FACTS</span> <span class="o">=</span> <span class="n">normalize_result</span><span class="p">(</span><span class="n">get_host_data_result</span><span class="p">)</span>
    <span class="n">TOPOLOGY_DETAILS</span> <span class="o">=</span> <span class="n">extract_lldp_details</span><span class="p">(</span><span class="n">GLOBAL_LLDP_DATA</span><span class="p">)</span>
    <span class="n">TOPOLOGY_DETAILS</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">GLOBAL_FACTS</span><span class="p">)</span>
    <span class="n">TOPOLOGY_DICT</span> <span class="o">=</span> <span class="n">generate_topology_json</span><span class="p">(</span><span class="o">*</span><span class="n">TOPOLOGY_DETAILS</span><span class="p">)</span>
    <span class="n">CACHED_TOPOLOGY</span> <span class="o">=</span> <span class="n">read_cached_topology</span><span class="p">()</span>
    <span class="n">write_topology_file</span><span class="p">(</span><span class="n">TOPOLOGY_DICT</span><span class="p">)</span>
    <span class="n">write_topology_cache</span><span class="p">(</span><span class="n">TOPOLOGY_DICT</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Open main.html in a project root with your browser to view the topology'</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">CACHED_TOPOLOGY</span><span class="p">:</span>
        <span class="n">DIFF_DATA</span> <span class="o">=</span> <span class="n">get_topology_diff</span><span class="p">(</span><span class="n">CACHED_TOPOLOGY</span><span class="p">,</span> <span class="n">TOPOLOGY_DICT</span><span class="p">)</span>
        <span class="n">print_diff</span><span class="p">(</span><span class="n">DIFF_DATA</span><span class="p">)</span>
        <span class="n">write_topology_file</span><span class="p">(</span><span class="n">DIFF_DATA</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">dst</span><span class="o">=</span><span class="s">'diff_topology.js'</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">topology_is_changed</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span><span class="s">'Open diff_page.html in a project root to view the changes.'</span><span class="p">)</span>
            <span class="k">print</span><span class="p">(</span><span class="s">"Optionally, open main.html and click 'Display diff' button"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># write current topology to diff file if the cache is missing
</span>        <span class="n">write_topology_file</span><span class="p">(</span><span class="n">TOPOLOGY_DICT</span><span class="p">,</span> <span class="n">dst</span><span class="o">=</span><span class="s">'diff_topology.js'</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">good_luck_have_fun</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="testing">Testing</h3>

<p>To begin with, let’s restrict the access to dist-rtr01 and run the script. Resulting topology:
<img src="https://habrastorage.org/webt/en/o9/n_/eno9n_hfmleczhlhik2oye3l0ew.png" alt="" />
<br />
Then let’s recover the access to dist-rtr02, restrict the access to edge-sw01, and execute the script again.<br />
The previous version becomes cached. The current topology looks as follows:
<img src="https://habrastorage.org/webt/wl/7e/pe/wl7epeuciuox-6deptnauipayzo.png" alt="" /></p>

<details>
<summary>Diff topology file diff_topology.js based on their comparison.</summary>

<pre>

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",
        }
    ]
};
</pre>

</details>

<p>A console output on the last run:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3.7 generate_topology.py 
Open main.html in a project root with your browser to view the topology

Topology changes have been discovered:

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
New network devices:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Hostname: dist-rtr01.devnet.lab

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Deleted devices:
vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Hostname: edge-sw01.devnet.lab

^^^^^^^^^^^^^^^^^^^^^
New interconnections:
vvvvvvvvvvvvvvvvvvvvv
From dist-rtr01.devnet.lab(Gi3) To core-rtr02.devnet.lab(Gi0/0/0/2)
From dist-rtr01.devnet.lab(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.lab(Eth1/3)
From dist-rtr01.devnet.lab(Gi2) To core-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.lab(Gi0/3) To core-rtr02.devnet.lab(Gi0/0/0/1)

Open diff_page.html to view the changes.
Optionally, open main.html and click the 'Display diff' button
</code></pre></div></div>
<p>Everything looks correct. The output matches the changes.<br />
To visualize the diff topology properly, we will make some adjustments in next_app.js below.<br /></p>

<h1 id="enhancing-next-ui-application">Enhancing NeXt UI application</h1>
<p>Most of the improvements below were made based on the examples in the Next UI documentation and tutorials.</p>

<h3 id="adding-interface-labels">Adding interface labels</h3>

<p>In order to add the interface labels, let’s extend the standard nx.graphic.Topology.Link class:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nx">nx</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">CustomLinkClass</span><span class="dl">'</span><span class="p">,</span> <span class="nx">nx</span><span class="p">.</span><span class="nx">graphic</span><span class="p">.</span><span class="nx">Topology</span><span class="p">.</span><span class="nx">Link</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">sourcelabel</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
            <span class="na">targetlabel</span><span class="p">:</span> <span class="kc">null</span>
        <span class="p">},</span>
        <span class="na">view</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">view</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">view</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
                <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">source</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">nx.graphic.Text</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">sourcelabel</span><span class="dl">'</span><span class="p">,</span>
                    <span class="dl">'</span><span class="s1">alignment-baseline</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text-after-edge</span><span class="dl">'</span><span class="p">,</span>
                    <span class="dl">'</span><span class="s1">text-anchor</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">start</span><span class="dl">'</span>
                <span class="p">}</span>
            <span class="p">},</span> <span class="p">{</span>
                <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">target</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">nx.graphic.Text</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">targetlabel</span><span class="dl">'</span><span class="p">,</span>
                    <span class="dl">'</span><span class="s1">alignment-baseline</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text-after-edge</span><span class="dl">'</span><span class="p">,</span>
                    <span class="dl">'</span><span class="s1">text-anchor</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">end</span><span class="dl">'</span>
                <span class="p">}</span>
            <span class="p">});</span>
            <span class="k">return</span> <span class="nx">view</span><span class="p">;</span>
        <span class="p">},</span>
        <span class="na">methods</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">update</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">inherited</span><span class="p">();</span>
                <span class="kd">var</span> <span class="nx">el</span><span class="p">,</span> <span class="nx">point</span><span class="p">;</span>
                <span class="kd">var</span> <span class="nx">line</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">line</span><span class="p">();</span>
                <span class="kd">var</span> <span class="nx">angle</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">angle</span><span class="p">();</span>
                <span class="kd">var</span> <span class="nx">stageScale</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">stageScale</span><span class="p">();</span>
                <span class="nx">line</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">pad</span><span class="p">(</span><span class="mi">18</span> <span class="o">*</span> <span class="nx">stageScale</span><span class="p">,</span> <span class="mi">18</span> <span class="o">*</span> <span class="nx">stageScale</span><span class="p">);</span>
                <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">sourcelabel</span><span class="p">())</span> <span class="p">{</span>
                    <span class="nx">el</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">view</span><span class="p">(</span><span class="dl">'</span><span class="s1">source</span><span class="dl">'</span><span class="p">);</span>
                    <span class="nx">point</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">start</span><span class="p">;</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">x</span><span class="dl">'</span><span class="p">,</span> <span class="nx">point</span><span class="p">.</span><span class="nx">x</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">y</span><span class="dl">'</span><span class="p">,</span> <span class="nx">point</span><span class="p">.</span><span class="nx">y</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">text</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">sourcelabel</span><span class="p">());</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">transform</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">rotate(</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">angle</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">point</span><span class="p">.</span><span class="nx">x</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">,</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">point</span><span class="p">.</span><span class="nx">y</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">)</span><span class="dl">'</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="nx">setStyle</span><span class="p">(</span><span class="dl">'</span><span class="s1">font-size</span><span class="dl">'</span><span class="p">,</span> <span class="mi">12</span> <span class="o">*</span> <span class="nx">stageScale</span><span class="p">);</span>
                <span class="p">}</span>
                <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">targetlabel</span><span class="p">())</span> <span class="p">{</span>
                    <span class="nx">el</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">view</span><span class="p">(</span><span class="dl">'</span><span class="s1">target</span><span class="dl">'</span><span class="p">);</span>
                    <span class="nx">point</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">end</span><span class="p">;</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">x</span><span class="dl">'</span><span class="p">,</span> <span class="nx">point</span><span class="p">.</span><span class="nx">x</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">y</span><span class="dl">'</span><span class="p">,</span> <span class="nx">point</span><span class="p">.</span><span class="nx">y</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">text</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">targetlabel</span><span class="p">());</span>
                    <span class="nx">el</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">transform</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">rotate(</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">angle</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">point</span><span class="p">.</span><span class="nx">x</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">,</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">point</span><span class="p">.</span><span class="nx">y</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">)</span><span class="dl">'</span><span class="p">);</span>
                    <span class="nx">el</span><span class="p">.</span><span class="nx">setStyle</span><span class="p">(</span><span class="dl">'</span><span class="s1">font-size</span><span class="dl">'</span><span class="p">,</span> <span class="mi">12</span> <span class="o">*</span> <span class="nx">stageScale</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>A customized link class can now be listed in the <strong>topo</strong> topology object properties.<br />
Let’s also highlight the links on the diff topology. New links will be green, deleted links will be red and dashed.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">linkConfig</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">linkType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">curve</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">sourcelabel</span><span class="p">:</span> <span class="dl">'</span><span class="s1">model.srcIfName</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">targetlabel</span><span class="p">:</span> <span class="dl">'</span><span class="s1">model.tgtIfName</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">style</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">model</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">model</span><span class="p">.</span><span class="nx">_data</span><span class="p">.</span><span class="nx">is_dead</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Deleted links contain 'is_dead' attribute set to 'yes'.</span>
            <span class="c1">// Make them dashed.</span>
            <span class="k">return</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">stroke-dasharray</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">5</span><span class="dl">'</span> <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">},</span>
    <span class="nx">color</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">model</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">model</span><span class="p">.</span><span class="nx">_data</span><span class="p">.</span><span class="nx">is_dead</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// Deleted links contain 'is_dead' attribute set to 'yes'</span>
            <span class="c1">// Make them red.</span>
            <span class="k">return</span> <span class="dl">'</span><span class="s1">#E40039</span><span class="dl">'</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">model</span><span class="p">.</span><span class="nx">_data</span><span class="p">.</span><span class="nx">is_new</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// New links contain 'is_new' attribute set to 'yes'</span>
            <span class="c1">// Make them green.</span>
            <span class="k">return</span> <span class="dl">'</span><span class="s1">#148D09</span><span class="dl">'</span>
        <span class="p">}</span>
    <span class="p">},</span>
<span class="p">},</span>
<span class="c1">// Use extended link class version with interface labels enabled</span>
<span class="nx">linkInstanceClass</span><span class="p">:</span> <span class="dl">'</span><span class="s1">CustomLinkClass</span><span class="dl">'</span> 
</code></pre></div></div>

<h3 id="adding-custom-node-icons">Adding custom node icons</h3>
<p>Next UI already contains a broad set of default icons for network devices.<br />
However, you are free to add custom icons based on your requirements. For deleted nodes, we need something special.<br />
To add a new icon, you should put an image into a directory accessible by Next UI and initialize it in <strong>topo</strong> object.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// the image is saved to ./img/dead_node.png</span>
<span class="nx">topo</span><span class="p">.</span><span class="nx">registerIcon</span><span class="p">(</span><span class="dl">"</span><span class="s2">dead_node</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">img/dead_node.png</span><span class="dl">"</span><span class="p">,</span> <span class="mi">49</span><span class="p">,</span> <span class="mi">49</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="getting-diff-visualization-done">Getting diff visualization done</h3>
<p>We have actually done everything we need. Let’s open <strong>diff_page.html</strong> and see how the changes we have done before look like:<br />
<img src="https://habrastorage.org/webt/yw/v6/zk/ywv6zkwkzsfn8euzb_rmlqmabq4.png" alt="" />
<br />
A topology view speaks for itself, doesn’t it?</p>

<h3 id="updating-a-node-tooltip">Updating a node tooltip</h3>
<p>By default, a node tooltip contains excessive information like internal node id and coordinates.<br />
In NeXt UI, it is possible to customize it for better readability and usability.<br />
Let’s include the following information for now:</p>
<ul>
  <li>Device hostname.<br />
Let’s also make hostname a customizable link to an arbitrary resource. It might be a device page in Netbox or a monitoring system.<br />
The link template will be stored in the dcimDeviceLink variable.<br />
It can be added during the topology generation process. In the case of missing value, the hostname will just be just a simple text.</li>
  <li>Device IP-Address, serial number, and model.</li>
</ul>

<p>In order to implement this, let’s extend a standard nx.ui.Component class and build a simle HTML form inside of it:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nx">nx</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">CustomNodeTooltip</span><span class="dl">'</span><span class="p">,</span> <span class="nx">nx</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">Component</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">node</span><span class="p">:</span> <span class="p">{},</span>
            <span class="na">topology</span><span class="p">:</span> <span class="p">{}</span>
        <span class="p">},</span>
        <span class="na">view</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">content</span><span class="p">:</span> <span class="p">[{</span>
                <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">div</span><span class="dl">'</span><span class="p">,</span>
                <span class="na">content</span><span class="p">:</span> <span class="p">[{</span>
                    <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">h5</span><span class="dl">'</span><span class="p">,</span>
                    <span class="na">content</span><span class="p">:</span> <span class="p">[{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">{#node.model.name}</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">props</span><span class="p">:</span> <span class="p">{</span><span class="dl">"</span><span class="s2">href</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{#node.model.dcimDeviceLink}</span><span class="dl">"</span><span class="p">}</span>
                    <span class="p">}],</span>
                    <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                        <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">border-bottom: dotted 1px; font-size:90%; word-wrap:normal; color:#003688</span><span class="dl">"</span>
                    <span class="p">}</span>
                <span class="p">},</span> <span class="p">{</span>
                    <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">,</span>
                    <span class="na">content</span><span class="p">:</span> <span class="p">[</span>
                        <span class="p">{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">IP: </span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">},</span> <span class="p">{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">{#node.model.primaryIP}</span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">}</span>
                    <span class="p">],</span>
                    <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                        <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">font-size:80%;</span><span class="dl">"</span>
                    <span class="p">}</span>
                <span class="p">},{</span>
                    <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">,</span>
                    <span class="na">content</span><span class="p">:</span> <span class="p">[</span>
                        <span class="p">{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Model: </span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">},</span> <span class="p">{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">{#node.model.model}</span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">}</span>
                    <span class="p">],</span>
                    <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                        <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">font-size:80%;</span><span class="dl">"</span>
                    <span class="p">}</span>
                <span class="p">},</span> <span class="p">{</span>
                    <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">,</span>
                    <span class="na">content</span><span class="p">:</span> <span class="p">[{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">S/N: </span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">},</span> <span class="p">{</span>
                        <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">label</span><span class="dl">'</span><span class="p">,</span>
                        <span class="na">content</span><span class="p">:</span> <span class="dl">'</span><span class="s1">{#node.model.serial_number}</span><span class="dl">'</span><span class="p">,</span>
                    <span class="p">}],</span>
                    <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                        <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">font-size:80%; padding:0</span><span class="dl">"</span>
                    <span class="p">}</span>
                <span class="p">},</span>
            <span class="p">],</span>
            <span class="na">props</span><span class="p">:</span> <span class="p">{</span>
                <span class="dl">"</span><span class="s2">style</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">width: 150px;</span><span class="dl">"</span>
            <span class="p">}</span>
        <span class="p">}]</span>
        <span class="p">}</span>
    <span class="p">});</span>

    <span class="nx">nx</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">Tooltip.Node</span><span class="dl">'</span><span class="p">,</span> <span class="nx">nx</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">Component</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">view</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">view</span><span class="p">){</span>
            <span class="nx">view</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
            <span class="p">});</span>
            <span class="k">return</span> <span class="nx">view</span><span class="p">;</span>
        <span class="p">},</span>
        <span class="na">methods</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">attach</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">inherited</span><span class="p">(</span><span class="nx">args</span><span class="p">);</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">model</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">});</span>
</code></pre></div></div>
<p>Now the custom class version can be listed in <strong>topo</strong> topology object properties:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">tooltipManagerConfig</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">nodeTooltipContentClass</span><span class="p">:</span> <span class="dl">'</span><span class="s1">CustomNodeTooltip</span><span class="dl">'</span>
<span class="p">},</span>
</code></pre></div></div>
<p>This is what we see on mouse click on the node after that:<br />
<img src="https://habrastorage.org/webt/hx/he/kl/hxheklf6kvjmz29kq48g38lu6qu.png" alt="" /></p>

<h3 id="customizing-node-layout">Customizing node layout</h3>

<p>As discussed before, the default Next UI data processor is ‘force’. It relies on the best effort algorithm spreading the Nodes so they would be as distant from each other as possible.<br /></p>

<p>This logic produces a proper layout even for complex hierarchical network topologies but the topology layers may not be oriented as desired. For sure, you can drag them manually afterward. However, this is not our way.<br />
<br />
Fortunately, there are some built-in tools to work with layers in Next UI.<br />
Let’s use a new numeric <strong>layerSortPreference</strong> attribute inside nodes in our Next UI application.<br />
The logic defining this value can be implemented outside the visualization application on the topology object generation stage in this case. Next UI would just order the layers in a way we tell it to. This is a more scalable approach.<br /></p>

<p>Let’s add some functions to be able to switch between layouts:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">var</span> <span class="nx">currentLayout</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">auto</span><span class="dl">'</span>
    <span class="nx">horizontal</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">currentLayout</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">horizontal</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="nx">currentLayout</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">horizontal</span><span class="dl">'</span><span class="p">;</span>
        <span class="kd">var</span> <span class="nx">layout</span> <span class="o">=</span> <span class="nx">topo</span><span class="p">.</span><span class="nx">getLayout</span><span class="p">(</span><span class="dl">'</span><span class="s1">hierarchicalLayout</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">layout</span><span class="p">.</span><span class="nx">direction</span><span class="p">(</span><span class="dl">'</span><span class="s1">horizontal</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">layout</span><span class="p">.</span><span class="nx">levelBy</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">node</span><span class="p">,</span> <span class="nx">model</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nx">model</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">layerSortPreference</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">});</span>
        <span class="nx">topo</span><span class="p">.</span><span class="nx">activateLayout</span><span class="p">(</span><span class="dl">'</span><span class="s1">hierarchicalLayout</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">};</span>
    <span class="nx">vertical</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">currentLayout</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">vertical</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="nx">currentLayout</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">vertical</span><span class="dl">'</span><span class="p">;</span>
        <span class="kd">var</span> <span class="nx">layout</span> <span class="o">=</span> <span class="nx">topo</span><span class="p">.</span><span class="nx">getLayout</span><span class="p">(</span><span class="dl">'</span><span class="s1">hierarchicalLayout</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">layout</span><span class="p">.</span><span class="nx">direction</span><span class="p">(</span><span class="dl">'</span><span class="s1">vertical</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">layout</span><span class="p">.</span><span class="nx">levelBy</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">node</span><span class="p">,</span> <span class="nx">model</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="nx">model</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">layerSortPreference</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">});</span>
        <span class="nx">topo</span><span class="p">.</span><span class="nx">activateLayout</span><span class="p">(</span><span class="dl">'</span><span class="s1">hierarchicalLayout</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">};</span>
</code></pre></div></div>
<p>Map these function to button elements on main.html and diff_page.html:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">'horizontal()'</span><span class="nt">&gt;</span>Horizontal layout<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"vertical()"</span><span class="nt">&gt;</span>Vertical layout<span class="nt">&lt;/button&gt;</span>
</code></pre></div></div>

<p>Let’s improve the <strong>generate_topology.py</strong> script and add some additional attributes to Nornir hosts file to implement an automatic node hierarchy calculation.<br />
The script will define an ordered list of human-friendly layer names and make a conversion into numeric values:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Topology layers would be sorted
# in the same descending order
# as in the tuple below
</span><span class="n">NX_LAYER_SORT_ORDER</span> <span class="o">=</span> <span class="p">(</span>
    <span class="s">'undefined'</span><span class="p">,</span>
    <span class="s">'outside'</span><span class="p">,</span>
    <span class="s">'edge-switch'</span><span class="p">,</span>
    <span class="s">'edge-router'</span><span class="p">,</span>
    <span class="s">'core-router'</span><span class="p">,</span>
    <span class="s">'core-switch'</span><span class="p">,</span>
    <span class="s">'distribution-router'</span><span class="p">,</span>
    <span class="s">'distribution-switch'</span><span class="p">,</span>
    <span class="s">'leaf'</span><span class="p">,</span>
    <span class="s">'spine'</span><span class="p">,</span>
    <span class="s">'access-switch'</span>
<span class="p">)</span>

<span class="k">def</span> <span class="nf">get_node_layer_sort_preference</span><span class="p">(</span><span class="n">device_role</span><span class="p">):</span>
    <span class="s">"""Layer priority selection function
    Layer sort preference is designed as a numeric value.
    This function identifies it by NX_LAYER_SORT_ORDER
    object position by default. With numeric values,
    the logic may be improved without changes on the NeXt app side.
    0(null) causes an undefined layer position in the NeXt UI.
    Valid indexes start with 1.
    """</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">role</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">NX_LAYER_SORT_ORDER</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">device_role</span> <span class="o">==</span> <span class="n">role</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">i</span>
    <span class="k">return</span> <span class="mi">1</span>
</code></pre></div></div>
<p>A numeric layer sort order for the layer will be defined by its relative position in NX_LAYER_SORT_ORDER.<br />
<strong>Important</strong>: NeXt UI interprets 0(null) as undefined. Valid layer indexes start with 1.<br /></p>

<p>The device layer will be based on its role attribute inside the Nornir Hosts Inventory file.<br />
<strong>data</strong> field allows us to specify a list of attributes with arbitrary names:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dist-rtr01</span><span class="pi">:</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">10.10.20.175</span>
    <span class="na">platform</span><span class="pi">:</span> <span class="s">ios</span>
    <span class="na">groups</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">devnet-cml-lab</span>
    <span class="na">data</span><span class="pi">:</span>
        <span class="na">role</span><span class="pi">:</span> <span class="s">distribution-router</span>
</code></pre></div></div>
<p>Any attribute inside host <strong>data</strong> can then be called in Python as a dict key as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nr</span><span class="p">.</span><span class="n">inventory</span><span class="p">.</span><span class="n">hosts</span><span class="p">[</span><span class="n">device</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'some_attribute_name'</span><span class="p">)</span>
</code></pre></div></div>

<p>To reflect the changes, let’s update our Python code. A new <em>nr_role</em> node attribute will be appended along with others into <em>global_facts</em> inside the <em>normalize_result</em> function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Full function is omitted here for brevity
</span><span class="n">global_facts</span><span class="p">[</span><span class="n">device_fqdn</span><span class="p">][</span><span class="s">'nr_role'</span><span class="p">]</span> <span class="o">=</span> <span class="n">nr</span><span class="p">.</span><span class="n">inventory</span><span class="p">.</span><span class="n">hosts</span><span class="p">[</span><span class="n">device</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'role'</span><span class="p">,</span> <span class="s">'undefined'</span><span class="p">)</span>
</code></pre></div></div>
<p>Then we should read this attribute during node object generation inside the <em>generate_topology_json</em> function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Full function is omitted here for brevity
</span><span class="n">device_role</span> <span class="o">=</span> <span class="n">facts</span><span class="p">[</span><span class="n">host</span><span class="p">].</span><span class="n">get</span><span class="p">(</span><span class="s">'nr_role'</span><span class="p">,</span> <span class="s">'undefined'</span><span class="p">)</span>
<span class="n">topology_dict</span><span class="p">[</span><span class="s">'nodes'</span><span class="p">].</span><span class="n">append</span><span class="p">({</span>
    <span class="s">'id'</span><span class="p">:</span> <span class="n">host_id</span><span class="p">,</span>
    <span class="s">'name'</span><span class="p">:</span> <span class="n">host</span><span class="p">,</span>
    <span class="s">'primaryIP'</span><span class="p">:</span> <span class="n">device_ip</span><span class="p">,</span>
    <span class="s">'model'</span><span class="p">:</span> <span class="n">device_model</span><span class="p">,</span>
    <span class="s">'serial_number'</span><span class="p">:</span> <span class="n">device_serial</span><span class="p">,</span>
    <span class="s">'layerSortPreference'</span><span class="p">:</span> <span class="n">get_node_layer_sort_preference</span><span class="p">(</span>
        <span class="n">device_role</span>
    <span class="p">),</span>
    <span class="s">'icon'</span><span class="p">:</span> <span class="n">get_icon_type</span><span class="p">(</span>
        <span class="n">lldp_capabilities_dict</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="s">''</span><span class="p">),</span>
        <span class="n">device_model</span>
    <span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p>And now we can <s>control the chaos</s> align the layers horizontally and vertically on button click. This is how it looks like:</p>

<p><img src="https://habrastorage.org/webt/b9/x6/4o/b9x64owgoavqcblnddnn6klfkye.gif" alt="Sample Layouts" /></p>

<h1 id="resulting-project-structure">Resulting project structure</h1>

<p>The final project structure looks as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ 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
</code></pre></div></div>

<h1 id="conclusion">Conclusion</h1>

<p>First of all, thank you for reading. I hope you enjoyed it.</p>

<p>In this article, I tried to reproduce and document the solution creation stages and my considerations behind them.</p>

<p>A full source code is available on my <a href="https://github.com/iDebugAll/devnet_marathon_endgame">GitHub</a> page.</p>

<p>The solution took first place on the Marathon based on attendees’ and organizers’ votes. And equally important, it has a potential for reuse and scalability(<em>spoiler</em>: I developed a <a href="https://github.com/iDebugAll/nextbox-ui-plugin">Netbox plugin</a> reusing the core code from this project).</p>

<p>How do you like the solution? What could be improved? How would you solve this task?
Please feel free to share your own experience and thoughts.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is a follow-up article on a local Cisco Russia DevNet Marathon online event I attended in May 2020. It was a series of educational webinars on network automation followed by daily challenges based on the discussed topics. On 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 task was definitely not trivial and not widely covered in public blog posts. In this article, I would like to break down my own solution that finally took first place and describe the selected toolset and considerations.]]></summary></entry></feed>