YAML – Yet Another YAML Lover

Python Logo

Sorry to keep on with the Junos EZ library stuff, but I’ve been having a lot of fun getting good results with minimal effort while playing with Jeremy Schulman’s Junos EZ Library.

This week I decided I wanted to write a program that would grab the routing table from a Junos router so I could look at monitoring differences before and after a change and see if anything in the routing table had changed.

Forget that this is Junos for a moment and imagine you could do this for any underlying router supporting NETCONF, and start thinking what you could do.

YAML Makes It Easy

One of the changes made recently was a move to YAML as the method for defining the way data is read from the Junos devices. The formatting is scarily intuitive, and once I got my mind around a little logic with Jeremy’s assistance, I was able to get just the report I wanted.

As it stands, the routes.yml file defines some tables that provide routing information, but I noted that the current configuration did not reference a specific routing-instance; a problem on my test system which has three virtual-routers in addition to the default inet.0.

Here’s an extract form the current routes.yml:

### ------------------------------------------------------
### show route <destination>
### ------------------------------------------------------

RouteTable:
  rpc: get-route-information
  args_key: destination
  item: route-table/rt 
  key: rt-destination
  view: RouteTableView

RouteTableView:
  groups:
    entry: rt-entry
  fields_entry:
    # fields taken from the group 'entry'
    protocol: protocol-name
    via: nh/via | nh/nh-local-interface

Let’s tear this apart a little for clarity. RouteTable is what gets referenced in Python – it’s going to issue a “get-route-information” RPC command, and the route destination rt-destination will be the default value put into the value .name. The view grabs a couple of fields from each rt-entry element, specifically the route protocol and the “via” – that is, the next hop interface. What else is missing? Yeah, the next hop IP… So let’s fix that.

YAML Hacking

Here’s my “fixed” YAML code. RouteTable doesn’t change, but RouteTableView does :

RouteTableView:
  groups:
    entry: rt-entry
  fields:
    table: ../table-name
  fields_entry:
    # fields taken from the group 'entry'
    protocol: protocol-name
    via: nh/via | nh/nh-local-interface
    nexthop: nh/to

Adding the IP next hope was easy – within the “nh” element is an element called “to”, which contains the next hop IP. Referencing the routing table took a little more effort, plus a slap in the head from Jeremy to get it right. Referencing XML elements – which is what the table and view do – allows you to go up a level using “..”. So in this case I have added a field called “table” which references the parent table name.

Of course all of this assumes that I understand the hierarchy of the data so let’s double check how I found this out?

Remember the XML RPC

Here’s an extract:

john@srx> show route | display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/10.3R2/junos">
    <route-information xmlns="http://xml.juniper.net/junos/10.3R2/junos-routing">
        <!-- keepalive -->
        <route-table>
            <table-name>inet.0</table-name>
            <destination-count>17</destination-count>
            <total-route-count>17</total-route-count>
            <active-route-count>17</active-route-count>
            <holddown-route-count>0</holddown-route-count>
            <hidden-route-count>0</hidden-route-count>
            <rt junos:style="brief">
                <rt-destination>1.1.1.0/24</rt-destination>
                <rt-entry>
                    <active-tag>*</active-tag>
                    <current-active/>
                    <last-active/>
                    <protocol-name>Direct</protocol-name>
                    <preference>0</preference>
                    <age junos:seconds="429207">4d 23:13:27</age>
                    <nh>
                        <selected-next-hop/>
                        <via>lt-0/0/0.0</via>
                    </nh>
                </rt-entry>
            </rt>

If we clean out some of the things we don’t care about, it looks like this:

        <route-table>
            <table-name>inet.0</table-name>
            <rt junos:style="brief">
                <rt-destination>1.1.1.0/24</rt-destination>
                <rt-entry>
                    <protocol-name>Direct</protocol-name>
                    <nh>
                        <via>lt-0/0/0.0</via>
                    </nh>
                </rt-entry>
            </rt>

The table name (inet.0) is one up in the hierarchy from the route destination (rt-destination). We want the processor to examine every route and return it to us, so for each route returned, it checks up a level and grabs the routing table name and adds it to the returned values.

When I first tried this I thought I’d have to create a new table and view that began a level higher, but realized with Jeremy’s help that the processor would just look once at each routing table element, so I wouldn’t get every route back. The code above checks each ad every route, but uses the “..” to check which routing table is in use and return it as part of the table.

Testing

Here’s the challenging Python code to read the routing table:

from jnpr.junos import Device
from jnpr.junos.op.routes import RouteTable

dev = Device("srx1", user="script", password="script123")
dev.open()

routes = RouteTable(dev)
routes.get()

for key in routes:
  print key.table, key.name, key.protocol, key.nexthop, '(',key.via,')'

Again, opening the device and grabbing the routing table is pretty much boilerplate. Then we print out the information found, including the routing tabs name, the destination, protocol, next hop, and the “via” interface. An extract of the output is below:

inet.0 192.168.102.1/32 Local None ( None )
inet.0 224.0.0.5/32 OSPF None ( None )
R1.inet.0 1.1.1.0/24 Direct None ( lt-0/0/0.1 )
R1.inet.0 1.1.103.0/24 OSPF 1.1.1.1 ( lt-0/0/0.1 )
R1.inet.0 10.0.1.0/24 OSPF 1.1.1.1 ( lt-0/0/0.1 )
R1.inet.0 10.0.2.0/24 OSPF 1.1.1.1 ( lt-0/0/0.1 )
R2.inet.0 1.1.103.0/24 OSPF 1.1.102.1 ( lt-0/0/0.120 )
R2.inet.0 10.0.1.0/24 OSPF 1.1.102.1 ( lt-0/0/0.120 )
R3.inet.0 1.1.1.0/24 OSPF 1.1.103.1 ( lt-0/0/0.130 )
R3.inet.0 1.1.13.0/24 Direct None ( lt-0/0/0.31 )

It’s not pretty (I didn’t work on formatting), but we are correctly grabbing all the routes known, along with the routing table they come from and the next hop IP.

Filtering Output

What if I only war to see the routing table for R1.inet.0? It turns out that this is directly supported in the Python command to get the routing table. Let’s back up a second and see what happens on the router itself when we view the routing table just for R1.inet.0:

john> show route table R1.inet.0 | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/10.3R2/junos">
    <rpc>
        <get-route-information>
                <table>R1.inet.0</table>
        </get-route-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

This shows the RPC command that is issued to implement the CLI command “show route table R1.inet.0”. You can see that within “get-route-information” there’s a “table” element whose value is “R1.inet.0”. So let’s try setting that in my Python code; this is the one line that needs to change:

routes.get(table="R1.inet.0")

By including this named value in parentheses, we pass that command to Junos, and we get back routes for R1.inet.0.

If I were to issue the same command but also add “1.1.1.2/32” to it (to view a single route), the XML RPC changes thus:

    <get-route-information>
            <destination>1.1.1.2/32</destination>
            <table>R1.inet.0</table>
    </get-route-information>

I have two choices then; I could add destination=“1.1.1.2/32” to my command. However, because in the YAML file “args_key” is set to “destination”:

  args_key: destination

…the default argument passed to the command will be destination. Thus our command can actually be this:

routes.get("1.1.1.2/32",table="R1.inet.0")

And the result is as predicted:

john@unix:~/jnpr$ python showroute.py
R1.inet.0 1.1.1.2/32 Local None ( lt-0/0/0.1 )

Moving On

I’m now well set to write my “before and after” script. What’s missing? Well, I might also want to monitor the age of routes – that’s returned within each route entry (rt-entry) element:

                <age junos:seconds="430670">4d 23:37:50</age>

It won’t take long to add this to the returned data. Of course, I’d quite like to grab the junos:seconds data because it saves me dealing with the converse of “4d 23:37:50” into something I can easily compare, and that will be my next challenge.

Junos EZ Library

As a reminder, Junos EZ library is being created and driven by Juniper’s Jeremy Schulman, who is looking for ways to make Junos devices (and ultimately any device) more accessible to non-programmers. If you want to try this out, I’ve included my own instructions for installing the Junos EZ library on Ubuntu.

Jeremy continues to develop the code (with some contributions from idiots like me via GitHub), and he’s also working on some documentation currently to help define some of this stuff more formally (it’s looking good so far!). More importantly, the more examples we put out there the easier it is for other people to give it a shot, and that’s one of the reasons I’m posting about the Junos EZ Library at the moment.

Fun Fun Fun!

It’s very satisfying to grab information from the routers with so little effort required. Right now I’m in regular programming mode (write, save, run, check output, repeat), but Jeremy also make a good case for a more interactive mode and I’ll cover that in a post soon, because it makes grabbing information even easier.

Meanwhile, if you have Juniper routers to play with, I encourage you to give this a shot. As I’ve said before, I am absolutely not a Python programmer; nonetheless, I’ve managed to write some code, update the YAML files to produce reports that I need, and even identify fixes and enhancements that I can submit via GitHub. I’ve even been making branches on GitHub to isolate my fixes from one another – meat for another post. Either way, the way this gets better is when people try it, find things that can be improved, and talk about it. Or, honestly, just appreciate how easy this is to do. Try it!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.