9.28.2009

Documenting Layer-3 Topologies

I recently decided to try making a set of scripts to generate a layer-3 diagram given a list of routers. My method was simple: obtain a list of connected routes and interfaces for each router in a list, find those networks that are shared by two or more routers, then spit this information out in a format that graphviz can use and generate a diagram. My goal was to make this work on a Metro Area Network of OSPF-speaking routers.

Now, if you're familiar with routing protocols, you might point out that retrieving connected routes from each router is wasteful; any one router with an OSPF process running in a given area will already know the complete topology for that area. And if you want the topologies of several areas, you just need to retrieve topology information from a few ABRs. I have two problems with this method. First, one of the things I had in mind when doing this was discovery: I would like to eventually discover the routers on the network rather than resort to maintaining a list. Finding ABRs is a more complex process than just noting any routers I come across. Indeed I doubt there would be any way of doing so without first discovering all of the routers on the network. Second, if I wanted to generate a topology for a network that used a distance-vector routing protocol, or only static routes, then the idea of extracting topology information from a single router goes right out the window.

If a third point could be made it is that I somehow find the idea of charting connected routes to be the most elegant solution. At the very least, it is probably the way I would document a layer-3 network if I had to by hand. Using CDP would require that I make too many "stops" along the way.

When I started this I had never worked with graphviz before. In fact, I didn't even have graphviz on my machine. Luckily some smart developers ported graphviz over to Mac OS. You can find the latest version here: http://www.pixelglow.com/graphviz/. For examples of syntax, I found this nice article: http://www.linuxjournal.com/article/7275. And for information about graphviz's wealth of attributes I used http://www.graphviz.org/doc/schema/attributes.xml.

After figuring out graphviz I now needed to write an Expect script to pull connected routes from a Cisco router. The code is straightforward:

#!/usr/bin/expect -f


if { $argv == "" } {
    puts "Usage: get_routes_conn.exp <router_ip> \[<router_ip>\]...\n"
    exit
}

set match ""
set prompt ""
set timeout 20

foreach rtr $argv {
    # Kludge for a problem where the use of continue
    # on the last iteration causes foreach to
    # run an iteration with a null list item. Bug in
    # TCL/Expect?
    if {$rtr == ""} {
        break
    }

    # Check if IP is well-formed
    regexp {[0-9]{1,3}(\.[0-9]{1,3}){3}} $rtr match
    if {$match == ""} {
        puts "$rtr is not a well-formed IP!\n"
        continue
    } else {set match ""}
    
    log_user 0
    puts ":$rtr -"
    spawn telnet $rtr
    expect {
        #For when a username implies priveleged mode
        "sername: " {
            send "jstorm\r"
            expect "assword: "
            send "somepassword\r"
            set prompt "#"
        }
        #For when no username implies unprivileged mode
        default {
            send "somepassword\r"
            set prompt ">"
        }
    }
    expect "$prompt"
    log_user 1
    send "sho ip route conn | incl ^C_\r"

    # Scroll through any paginated output
    while {1} {
        expect {
            --
            " --More-- " {send " "}
            "$prompt" {break}
        }
    }
    log_user 0
    send "exit\r"
    close
    puts ""
}
exit

When you feed this script the IP of a Cisco router or layer-3 switch running IOS it should produce output similar to the following:

:172.17.0.207 -
sho ip route conn | incl ^C_
C 172.171.0.207/32 is directly connected, Loopback0
C 172.20.0.207/32 is directly connected, Loopback1
C 172.25.207.0/24 is directly connected, FastEthernet1/1
TUNNEL-rtr#

Hooray! We have connected routes and interfaces! Of course, the problem then becomes turning that output into this:

"172.25.207.0/24" [label="172.25.207.0/24", shape=ellipse, \
fillcolor="#ecd9cc"];

"172.17.0.207" [label="TUNNEL-rtr\n172.17.0.207", shape=box, fillcolor="#ccd9ec", \
URL="telnet://172.17.0.207"];

"172.17.0.207" -- "172.25.207.0/24" [label="Fa 1/1"];

A messy affair, indeed. Fortunately, I've written a BASH script that does exactly this:

#!/bin/bash
#
# FILENAME: make_rtr_topo.sh
# DESCRIPTION: Uses get_routes_conn.exp to create a diagram from a list of given routers.
# LAST UPDATE: 2009-09-27 - jds (Lots of things)
#


EXPFILE='/home/jstorm/scripts/get_routes_conn.exp'

if [ -z $1 ]; then
    echo "Usage: make_rtr_topo.sh <router_ip> [<router_ip>]...\n"
    exit
fi


#Set attributes for graphviz output
NETATTR='[label=\"\1\", shape=ellipse, fillcolor="#ecd9cc"];'
RTRATTR='[label=\"\2\\n\1\", shape=box, fillcolor="#ccd9ec", URL=\"telnet:\/\/\1\"];'
EDGEATTR='[label=\"\2\"];';

nets=""
cnxnlist=""

#graphviz graph header
echo "graph BACKBONE"
echo "{"
echo " edge [len=2.5];"
echo " node [style=filled];"
echo " size=\"25,20\";"
echo " splines=true;"
echo " normalize=true;"
echo " overlap=false;"
echo " comment=\"BACKBONE - `date '+%m/%d/%Y %H:%M'`\";"
echo

#Fetch connected routes, extracting the hostname, routes, and interfaces;
# format as NEATO node or edge notation
for ip in $@; do
    tmp="`${EXPFILE} $ip`"
    hostname="`echo \"$tmp\" | tail -n 1 | sed -e 's/[>#]$//'`"
    tmp2="`echo \"$tmp\" | egrep '^:|^C' | sed -e 's/^C *//' \
     -e 's/ is .*, \([a-zA-Z][a-zA-Z]\)[a-zA-Z]*\([0-9]*\(\/*[0-9]*\)*\.*[0-9]*\)^M$/,\1 \2/'`"
    router=`echo -n "$tmp2" | head -n 1 | sed -e 's/://' -e "s/ -$/,$hostname/"`
    nets="`echo \"$nets\"; echo \"$tmp2\" | tail -n +2`"
    cnxnlist="`echo \"$cnxnlist\"; echo \"$tmp2\" | tail -n +2 | \
     sed -e \"s/^/$router -- /\"`"
done
nets="`echo \"$nets\" | sed -e 's/,.*$//'`"

#Prune networks not common between two or more routers
# (i.e. DO NOT display all routed networks, lest it fries your brain)
nets="`echo \"$nets\" | sort | uniq -d`"

#Prune edges whose network nodes are not defined
tmplist=""
for net in `echo $nets`; do
    tmplist="`echo \"$tmplist\"; echo \"$cnxnlist\" | grep $net`"
done
cnxnlist="`echo \"$tmplist\" | sort | uniq`"

#Output network nodes
echo "$nets" | sed -e "s/^\(.*\)$/\"\1\" $NETATTR/" | sed -e 's/^/ /'
echo

#Output router nodes
echo "$cnxnlist" | tail -n +2 | sed -e 's/ --.*$//' | sort | uniq | \
sed -e "s/^\(.*\),\([0-9a-zA-Z\-]*\)/\"\1\" $RTRATTR/" | sed -e 's/^/ /'
echo

#Output router-to-network node edges
echo "$cnxnlist" | tail -n +2 | sed -e 's/^\(.*\),\([0-9a-zA-Z\-]*\) --/\"\1\" --/' \
-e "s/-- \(.*\),\(.*\)/-- \"\1\" $EDGEATTR/" | sed -e 's/^/ /'
echo "}"

As you may already have guessed, this script relies heavily on sort, assuming that it will sort deterministically. If you see differences in the sort that is produced between different machines (as I did) then you may consider setting the LC_COLLATE environment variable. I prefer that LC_COLLATE be set to 'C', making '1' come before '10' and so on. If you would like to see the existing locale setting, try issuing the locale command; env may also be a good place to look.

With the output from make_rtr_topo.sh redirected to a file, graphviz's NEATO takes care of the rest. For output to an SVG use the following:
neato -ol3topo.svg -Tsvg l3topo.dot

Here, 'l3topo.svg' is the output file, '-T' specifies the output format, and 'l3topo.dot' contains the output from make_rtr_topo.sh. Easy, eh?

Here is an example of the output.

Cheers.

No comments:

Post a Comment