Most people that start out working with Junos using PyEZ seem to get stuck trying to figure out how to retrieve information. Since I always learn the most from short examples that I can reverse engineer or alter to fit my needs, I aim to provide you with just that.
In this article, I will use PyEZ to retrieve OSPF information from multiple devices running Junos. In the example, I will use both the findall
as well as the find
methods from the lxml
module. The reason for using these two methods is that they cover most of the situations that I have run into. We will use these methods to iterate a list of OSPF neighbors and then extract information from individual OSPF neighbors. The logic applied there is similar to what you’ll need when you examine BGP sessions, interfaces, line-cards, LSPs, etc.
Retrieving OSPF information
In this example, we are going after the neighbor id, neighbor address, interface and neighbor adjacency-time. Using the CLI, we can obtain this information by issuing the show ospf neighbor extensive
command. To figure out what RPC we need, we simply issue show ospf neighbor extensive |display xml rpc
:
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1F4/junos">
<rpc>
<get-ospf-neighbor-information>
<extensive/>
</get-ospf-neighbor-information>
</rpc>
</rpc-reply>
What is enclosed in the rpc tag will translate to get_ospf_neighbor_information(extensive=True)
in our Python script. To figure out what data to extract from the return output, we issue the show ospf neighbor extensive |display xml
command (output shortened to keep it readable):
said@ar01.ams> show ospf neighbor extensive |display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1F4/junos">
<ospf-neighbor-information xmlns="http://xml.juniper.net/junos/15.1F4/junos-routing">
<ospf-neighbor>
<neighbor-address>10.253.158.131</neighbor-address>
<interface-name>ae11.0</interface-name>
<neighbor-id>10.253.158.254</neighbor-id>
<neighbor-adjacency-time junos:seconds="65269304">
107w6d 10:21:44
</neighbor-adjacency-time>
</ospf-neighbor>
<ospf-neighbor>
<neighbor-address>10.253.158.149</neighbor-address>
<interface-name>ae12.0</interface-name>
<neighbor-id>10.253.158.253</neighbor-id>
<neighbor-adjacency-time junos:seconds="65183944">
107w5d 10:39:04
</neighbor-adjacency-time>
</ospf-neighbor>
</rpc-reply>
The previous output displays the element nodes we are interested in:
- neighbor-id
- neighbor-address
- interface-name
- neighbor-adjacency-time
For the first three items, we will extract the text-nodes. For the neighbor-adjacency-time
however, the attribute node (junos:seconds="65183944"
) is more interesting. Having just an integer is a lot easier to work with when compared to a string that may or may not contain w
or d
.
We want to retrieve this information for every adjacency and we need to return the information in a way that we can use it later on. We will start off with a function that collects and returns the relevant information for 1 device and have it do the following:
- Log into the node
- Issue the RPC
- Iterate all the OSPF adjacencies
- Extract the information for every adjacency that is found
- Return this information in a dictionary
The example I came up with is the following:
def jun_ospf_neighbor_extensive(username, pwd, host ):
return_dict = {}
dev = Device(host=host, user=username, password=pwd)
dev.open()
ospf_information = dev.rpc.get_ospf_neighbor_information(extensive=True)
dev.close()
ospf_neighbors = ospf_information.findall('.//ospf-neighbor')
for neighbor in ospf_neighbors:
neighbor_id = neighbor.find('.//neighbor-id').text
address = neighbor.find('.//neighbor-address').text
interface = neighbor.find('.//interface-name').text
uptime = neighbor.find('.//neighbor-adjacency-time').attrib['seconds']
return_dict[interface] = {
'neighbor-id' : neighbor_id,
'neighbor-address' : address,
'interface-name' : interface,
'neighbor-adjacency-time' : uptime,
}
return return_dict
Let’s break this down and describe what is happening.
The following is used to open a connection to the device, issue the RPC, store the device response in the ospf_information
variable and close the connection:
dev.open()
ospf_information = dev.rpc.get_ospf_neighbor_information(extensive=True)
dev.close()
The information that is now stored in ospf_information
equates to the entire output of the show ospf extensive | display xml
command. What we need to do is iterate all the OSPF neighbors so that we can retrieve information for every individual neighbor. To this end, we turn to the findall
method:
ospf_neighbors = ospf_information.findall('.//ospf-neighbor')
The findall
method is used to return a list of matching elements. In this case, the matching element is the ospf-neighbor
.
The list that findall
returns is stored in ospf_neighbors
. If we would print the type of every item in the list returned by findall
, it would show <type 'lxml.etree._Element'>
.
In case we wanted to see how the information inside every item of the returned list looks, we could decide to use etree.tostring
. This requires us putting in from lxml import etree
as well as adding the following code right after using findall
:
for neighbor in ospf_neighbors:
print(etree.tostring(neighbor, pretty_print=True))
With this addition, we can have a look at the content (shortened to keep it readable):
<ospf-neighbor>
<neighbor-address>10.97.18.248</neighbor-address>
<interface-name>ae6.0</interface-name>
<neighbor-id>10.45.16.30</neighbor-id>
<neighbor-adjacency-time seconds="24429948">
40w2d 18:05:48
</neighbor-adjacency-time>
</ospf-neighbor>
<ospf-neighbor>
<neighbor-address>10.253.158.129</neighbor-address>
<interface-name>ae7.0</interface-name>
<neighbor-id>10.253.158.252</neighbor-id>
<neighbor-adjacency-time seconds="92141820">
152w2d 10:57:00
</neighbor-adjacency-time>
</ospf-neighbor>
From the complete XML that was returned by the Juniper device, we managed to extract a list of XML objects. Every object contains information on a single OSPF neighbor. We can use the find
method to search the every item for the three text nodes and the attribute node we need:
for neighbor in ospf_neighbors:
neighbor_id = neighbor.find('.//neighbor-id').text
address = neighbor.find('.//neighbor-address').text
interface = neighbor.find('.//interface-name').text
uptime = neighbor.find('.//neighbor-adjacency-time').attrib['seconds']
The last part of the function is storing these values in a dictionary. We instantiated that dictionary a little earlier when we used return_dict = {}
at the beginning of the function. Now, while we are still inside the for loop, we store the variables in that dictionary like so:
return_dict[interface] = {
'neighbor-address' : address,
'interface-name' : interface,
'neighbor-adjacency-time' : uptime,
}
The last return return_dict
statement returns the dictionary for future use.
An easy way to run this function in a script would be the following:
from jnpr.junos import Device
from pprint import pprint
from lxml import etree
def jun_ospf_neighbor_extensive(username, pwd, host ):
return_dict = {}
dev = Device(host=host, user=username, password=pwd)
dev.open()
ospf_information = dev.rpc.get_ospf_neighbor_information(extensive=True)
dev.close()
ospf_neighbors = ospf_information.findall('.//ospf-neighbor')
for neighbor in ospf_neighbors:
#print(etree.tostring(neighbor, pretty_print=True))
neighbor_id = neighbor.find('.//neighbor-id').text
address = neighbor.find('.//neighbor-address').text
interface = neighbor.find('.//interface-name').text
uptime = neighbor.find('.//neighbor-adjacency-time').attrib['seconds']
return_dict[interface] = {
'neighbor-id' : neighbor_id,
'neighbor-address' : address,
'interface-name' : interface,
'neighbor-adjacency-time' : uptime,
}
return return_dict
if __name__ == "__main__":
# to run the function against a single host
import sys
host = str(sys.argv[1])
pprint(jun_ospf_neighbor_extensive('username_123', 'password_123', host ))
Because we are using sys.argv
, we can target individual devices when we run the script like so:
[said@server]$ python get_ospf.py ar01.ams
{'ae11.0': {'interface-name': 'ae11.0',
'neighbor-address': '10.253.158.131',
'neighbor-adjacency-time': '66251082',
'neighbor-id': '10.253.158.254'},
'ae12.0': {'interface-name': 'ae12.0',
'neighbor-address': '10.253.158.149',
'neighbor-adjacency-time': '66165722',
'neighbor-id': '10.253.158.253'},
'ae4.0': {'interface-name': 'ae4.0',
'neighbor-address': '10.253.158.135',
'neighbor-adjacency-time': '18649136',
'neighbor-id': '10.253.158.250'},
'ae7.0': {'interface-name': 'ae7.0',
'neighbor-address': '10.253.158.129',
'neighbor-adjacency-time': '93122299',
'neighbor-id': '10.253.158.252'}}
Checking multiple nodes
Let’s enable our script to collect information from multiple devices. The following function will make it easy to iterate a list of devices, call the function we have made earlier and store the output in a single dictionary:
def jun_ospf_neighbor_extensive_network(username, pwd, hosts = []):
network_ospf_dict = {}
for host in hosts:
ospf_dict = jun_ospf_neighbor_extensive(username, pwd, host )
network_ospf_dict[host] = ospf_dict
return network_ospf_dict
This function takes in a username, password and a list of hosts. It starts by instantiating a dictionary called network_ospf_dict
. After that, it will iterate the list of hosts. For every host in the list, it will run jun_ospf_neighbor_extensive
. The returned output is stored in the network_ospf_dict
which is returned in the end.
Let’s clean up all references to etree
, add the new function and change the __main__
. We now have the following script:
from jnpr.junos import Device
from pprint import pprint
def jun_ospf_neighbor_extensive(username, pwd, host ):
return_dict = {}
dev = Device(host=host, user=username, password=pwd)
dev.open()
ospf_information = dev.rpc.get_ospf_neighbor_information(extensive=True)
dev.close()
ospf_neighbors = ospf_information.findall('.//ospf-neighbor')
for neighbor in ospf_neighbors:
neighbor_id = neighbor.find('.//neighbor-id').text
address = neighbor.find('.//neighbor-address').text
interface = neighbor.find('.//interface-name').text
uptime = neighbor.find('.//neighbor-adjacency-time').attrib['seconds']
return_dict[interface] = {
'neighbor-id' : neighbor_id,
'neighbor-address' : address,
'interface-name' : interface,
'neighbor-adjacency-time' : uptime,
}
return return_dict
def jun_ospf_neighbor_extensive_network(username, pwd, hosts = []):
network_ospf_dict = {}
for host in hosts:
ospf_dict = jun_ospf_neighbor_extensive(username, pwd, host )
network_ospf_dict[host] = ospf_dict
return network_ospf_dict
if __name__ == "__main__":
hosts = ['ar03.ams', 'pr02.lon04', ]
pprint(jun_ospf_neighbor_extensive_network('username_123', 'password_123', hosts))
When we run it against two hosts, we get the following result:
{'ar03.ams': {'ae11.0': {'interface-name': 'ae11.0',
'neighbor-address': '10.8.198.131',
'neighbor-adjacency-time': '74643455',
'neighbor-id': '10.8.198.254'},
'ae12.0': {'interface-name': 'ae12.0',
'neighbor-address': '10.8.198.135',
'neighbor-adjacency-time': '7350164',
'neighbor-id': '10.8.198.253'},
'ae8.0': {'interface-name': 'ae8.0',
'neighbor-address': '10.8.198.141',
'neighbor-adjacency-time': '74643488',
'neighbor-id': '10.8.198.250'},
'ae9.0': {'interface-name': 'ae9.0',
'neighbor-address': '10.8.198.143',
'neighbor-adjacency-time': '74643490',
'neighbor-id': '10.8.198.249'}},
'pr02.lon04': {'ae0.12': {'interface-name': 'ae0.12',
'neighbor-address': '192.254.3.84',
'neighbor-adjacency-time': '41364841',
'neighbor-id': '10.0.140.234'},
'ae1.12': {'interface-name': 'ae1.12',
'neighbor-address': '192.254.4.84',
'neighbor-adjacency-time': '41364843',
'neighbor-id': '10.0.140.235'},
'ae101.0': {'interface-name': 'ae101.0',
'neighbor-address': '192.254.35.71',
'neighbor-adjacency-time': '32976830',
'neighbor-id': '10.0.140.236'},
'ae42.0': {'interface-name': 'ae42.0',
'neighbor-address': '192.254.9.123',
'neighbor-adjacency-time': '40529384',
'neighbor-id': '10.0.141.255'}}}
Wrapping up
We wrote a function that retrieves OSPF information by talking to the Juniper API. We used 2 methods from the lxml
module to deal with the XML and retrieve the information we needed.
First we used findall
. This gave us a list with information on individual OSPF neighbors. After this, we used find
to get the exact information we needed from every individual neighbor. We finished up showing how to get the information for multiple devices.
You can search for the Junos PyEZ Developer Guide
for more information on PyEZ. To better understand how to extract information from the Junos API responses, look at the XPath support lxml
has to offer and what other methods you might be able to use.
Working with the Junos API has always been immensely satisfying to me. I hope this article gave you some insights and ideas on how to get started.