Genja logo

Junos PyEZ XPath Expressions

video

Genja tutorials pt 4.png

by Andre Bowen-Brown

July 19, 2021

Understand how to manipulate RPC calls for the finer details

Continuing from where we left the last blog, we will dive deeper into the Python lmxl module and XML's XPath expressions. We now have a tool that provides us with the correct RPC's. Let's put it to use.

Ok, so we now have an RPC to call, thanks to our RPC discovery tool, created in the previous blog, Junos PyEZ RPC Discovery Tool. We will issue the show system uptime RPC get_system_uptime_information. This RPC contains more information, with some of the elements containing XML attributes. We've already had a taste of using lxml's findtext method using XPath's rather basic expression. We will expand on on XPath's expressions and the other methods available.


Dumping the returned rpc-reply

If you want to dump the full XML rpc-reply, the actual lxml module has a method for that. It can come in handy for building your XPath expression's knowing where the tags reside with the tree. Let's import the module to dump the contents of the get_system_uptime_information lxml object.

  • from lxml import etree - imports the etree API
  • etree.dump(object) - prints the content of the lxml.etree.Element object. You don't need to use print when using this function, otherwise, you'll get None at the end

We'll copy connect_ssh_key.py to a new file called lxml_uptime.py

Let's see it in use

cp connect_ssh_key.py lxml_uptime.py

Open the new file lxml_uptime.py for editing. We'll call the latest RPC get_system_uptime_information import etree and dump the contents of the returned object.

This is what the final file should look like

from jnpr.junos import Device
from getpass import getpass
import jnpr.junos.exception
from lxml import etree

ipadd = input('Enter hostname or IP address: ')
key_password = getpass('Enter key password: ')

try:
    with Device(host=ipadd, password=key_password) as dev:
        print('Are we connected?', dev.connected)
        sys_uptime = dev.rpc.dev.rpc.get_system_uptime_information()
        etree.dump(sys_uptime)
except jnpr.junos.exception.ConnectAuthError as autherr:
    print('Check key password', autherr)

Truncated output

<system-uptime-information>
truncated...

  <uptime-information>
    <date-time seconds="1626690023">
10:20AM
</date-time>
    <up-time seconds="1972012">
22 days, 19:46
</up-time>
    <active-user-count format="2 users">
2
</active-user-count>
    <load-average-1>
0.25
</load-average-1>
    <load-average-5>
0.20
</load-average-5>
    <load-average-15>
0.17
</load-average-15>
  </uptime-information>

Now we can see what data we can parse using the array XPath syntax available, but there is a slight problem. Some of the output is all over the place! Opening and closing tags for some elements are on separate lines from the text. i.e., <uptime-information> <up-time>


Normalising an RPC response

The reason is that some of the text contains whitespace, e.g., \n (newline) before and after the text.

We can check for the white space using the Python repr function. First, we need to grab the text from one of the elements. Let's get the text of <uptime-information> <up-time>.

Add to line 13 after sys_uptime

uptime = sys_uptime.findtext("uptime-information/up-time")
print("\n" + repr(uptime))
print()

Remove

etree.dump(sys_uptime)

Output

lxml output whitespace.png

Luckily PyEZ has a feature to resolve this issue. When initiating the RPC call, you can use the keyword argument normalize=True, as it is disabled by default unless you are using tables and views. Adding this to our code, we won't suffer this manifestation of crap on the page!

From the output, we can certainly see the whitespace.

Let's fix it!

sys_uptime = dev.rpc.get_system_uptime_information(normalize=True)

Standardise the normalisation argument

As the white space in the text values is unpredictable, you should make it standard practice adopting normalize=True within all of your RPC calls. Negating it will only cause frustration.

Now let's run the script again and gather some facts.

Output

lxml output removed whitespace.png

The whitespace either side of the text has now be removed. If we remove the findtext line of code, we will now get the entirety of get_software_information.

<system-uptime-information>
truncated...

  <uptime-information>
    <date-time seconds="1626696667">12:11PM</date-time>
    <up-time seconds="1978656">22 days, 21:37</up-time>
    <active-user-count format="2 users">2</active-user-count>
    <load-average-1>0.15</load-average-1>
    <load-average-5>0.19</load-average-5>
    <load-average-15>0.17</load-average-15>
  </uptime-information>
</system-uptime-information>


Now we can see the XML elements

We already have experience using the findtext method that returns a string. Below is a list of the different find variants

  • findtext - returns a string
  • find - returns an lxml.etree.Element objects
  • findall - returns a list of lxml.etree.Element objects

Each method has its benefit. For instance, findall can list all the interface names which are operationally up. I'll introduce a new XPath expression [tag='value']. It selects all immediate child elements that are equal to the tag and value, e.g., [oper-status='up']

Just like any Python list, you'll have to iterate over it to gain access to the results, if any. Then you can access the lxml.etree.Element object text, using the text method.

find interface.png

Finding interfaces

Off the back of the example above well be parsing the operational state of interfaces. Using the RPC Discovery tool, show interfaces = get_interface_information.

We'll dump the contents of the get_interface_information RPC to find what elements and attributes we can use in our XPath expression for parsing the interfaces.

interfaces = dev.rpc.get_interface_information(normalize=True)
etree.dump(interfaces)

Truncated output

I'm not going to display all of the rpc-reply, but we now have enough information above to create the XPath expression to find all the operational interfaces that are up to retrieve their names.

<interface-information style="normal">
  <physical-interface>
    <name>ge-0/0/0</name>
    <admin-status format="Enabled">up</admin-status>
    <oper-status>up</oper-status>
    <local-index>138</local-index>
    <snmp-index>513</snmp-index>
    <description>sky-router 1</description>
    <link-level-type>Ethernet</link-level-type>
    <mtu>1514</mtu>

truncated...

Employing the findall method. We will use the following to filter the results based on the operational status being up. Then using a for loop, print out the name of the gigabit ethernet interfaces.

  • [oper-status='up'] - finds element named oper-status with the text = up
  • name - the interface name
interface_list = interfaces.findall(
        "physical-interface/[oper-status='up']/name")
for interface in interface_list:
    if "ge-" in interface.text:
        print(interface.text)

Result

In my instance, I only have one active port: ge-0/0/0.

How do we access attributes?

Accessing attributes is achieved by using a method called attrib on an lxml.etree.Element object. This can be demonstrated using the find method, which returns an object. The attribute needs to exist for the particular element otherwise there will be a KeyError raised. We'll amend the lxml_uptime.py file and add the total number of seconds the device has been online.

  • attrib['seconds'] - the method used to retrieve an attribute from an element, 'seconds' is the name of the particular attribute we want
  • print() - prints a blank line for styling purposes

Add

seconds = sys_uptime.find("uptime-information/up-time").attrib['seconds']
print(f"Total in seconds = {seconds}")
print()

*Note: you could also use the method against the seconds variable i.e., print(seconds.attrib['seconds'])

Final configuration

You can also download the code from my GitHub repository lxml_get_uptime.py.

from jnpr.junos import Device
from getpass import getpass 
import jnpr.junos.exception
from lxml import etree

ipadd = input('Enter hostname or IP address: ')
key_password = getpass('Enter key password: ')

try:
    with Device(host=ipadd, password=key_password) as dev:
        print('Are we connected?', dev.connected)
        sys_uptime = dev.rpc.get_system_uptime_information(normalize=True)
        uptime = sys_uptime.findtext("uptime-information/up-time")
        seconds = sys_uptime.find(
                "uptime-information/up-time").attrib['seconds']
        print("\n" + repr(uptime))
        print(f"Total in seconds = {seconds}")
        print()
except jnpr.junos.exception.ConnectAuthError as autherr:
    print('Check key password', autherr)

Final result

lxml uptime final output.png

The End

There we have it. We've covered several different RPC methods and quite a few XPath expressions to manipulate the data for a concise output.