Getting Started With Juniper PyEZ Library
In this blog post, we're diving into how to use the PyEZ Python library to interact with Juniper devices. I'll be working with a Juniper vMX device as our example, but PyEZ can work with any other Junos-based device. So, whether you have a vMX, an SRX, or any other Junos device, you'll find this guide helpful.
What we will cover?
- What is Juniper PyEZ?
- Why do we need PyEZ?
- Prerequisites
- Getting facts from Juniper vMX
- Getting Interface Stats and Errors
- A few things to note
- Closing thoughts
What is Juniper PyEZ?
Junos PyEZ is a microframework for Python that enables you to manage and automate Junos devices. Junos PyEZ is designed to provide the capabilities that we would typically get from the CLI.
You can use Junos PyEZ to retrieve facts or operational information from a device, execute remote procedure calls (RPC) available through the Junos XML API and even install or upgrade the Junos software. But for the sake of this example, we will retrieve the facts from the vMX and then retrieve some interface statistics.
But, Why Do I need PyEZ?
If you're wondering why we need PyEZ, here's a straightforward reason from my experience. I often run show commands on devices and then need to parse the output. Normally, I'd use Netmiko for the commands and TextFSM for parsing. But I hit a snag when I needed to parse the output of show interfaces extensive
because the ntc_templates library didn't have a parser for it. That's when I turned to PyEZ and found it already had a table available for this command. It was a simple and effective solution for my specific needs.
Prerequisites
For this blog post, I'm assuming you have a basic understanding of Python and Junos. Before diving in, you'll need to ensure that netconf-over-ssh is enabled on your Junos device, as shown below.
[edit]
admin@mx-router-01# show system services
ssh;
netconf {
ssh;
}
After setting that up, just install PyEZ on your system by running a pip install.
pip install junos-eznc
Getting Facts from vMX
To kick things off and ensure everything is set up correctly, we'll start with a straightforward script. This script will connect to a vMX device and retrieve its facts using the PyEZ library. Here's the script.
from jnpr.junos import Device
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
facts = device.facts
print(json.dumps(dict(facts), indent=2, default=str))
device.close()
- Import Libraries - First, we import the necessary libraries.
Device
Class fromjnpr.junos
is used for connecting to and interacting with Junos devices.json
is used for formatting the output in a readable manner. - Connect to the Device - We create an instance of the
Device
class with the vMX's IP address, username, and password. This instance is used to establish a connection to the device. - Retrieve Device Facts - Once connected, we use the
.facts
attribute of theDevice
object to get a collection of facts about the device. This includes details like the device model, serial number, operating system version, and more. - Print the Facts - The
device.facts
returns a_FactCache
object, which behaves much like a dictionary but with some additional functionalities used for Junos PyEZ. Since_FactCache
isn't directly serializable to JSON format, we convert it to a dictionary first. However, not all objects within this dictionary can be easily converted to a JSON string. To overcome this, we usejson.dumps
withdefault=str
, which tells the JSON serializer to convert any non-serializable objects it encounters into strings. This is particularly useful for ensuring that all elements of thefacts
dictionary can be serialized without errors. - Close the Connection - Finally, we close the connection to the device. It's a good practice to close connections gracefully.
{
"current_re": [
"re0",
"master",
"node",
"fwdd",
"member",
"pfem"
],
"domain": "packet.lan",
"fqdn": "mx-router-01.packet.lan",
"switch_style": "BRIDGE_DOMAIN",
"HOME": "/var/home/admin",
"srx_cluster": null,
"srx_cluster_id": null,
"srx_cluster_redundancy_group": null,
"RE_hw_mi": false,
"serialnumber": "VM642FCB4DDA",
"2RE": false,
"master": "RE0",
"RE0": {
"mastership_state": "master",
"status": "OK",
"model": "RE-VMX",
"last_reboot_reason": "Router rebooted after a normal shutdown.",
"up_time": "21 minutes, 12 seconds"
},
"RE1": null,
"re_info": {
"default": {
"0": {
"mastership_state": "master",
"status": "OK",
"model": "RE-VMX",
"last_reboot_reason": "Router rebooted after a normal shutdown."
},
"default": {
"mastership_state": "master",
"status": "OK",
"model": "RE-VMX",
"last_reboot_reason": "Router rebooted after a normal shutdown."
}
}
},
"re_master": {
"default": "0"
},
"junos_info": {
"re0": {
"text": "18.2R1.9",
"object": "junos.version_info(major=(18, 2), type=R, minor=1, build=9)"
}
},
"hostname": "mx-router-01",
"hostname_info": {
"re0": "mx-router-01"
},
"model": "VMX",
"model_info": {
"re0": "VMX"
},
"version": "18.2R1.9",
"version_info": "junos.version_info(major=(18, 2), type=R, minor=1, build=9)",
"version_RE0": "18.2R1.9",
"version_RE1": null,
"vc_capable": false,
"vc_mode": null,
"vc_fabric": null,
"vc_master": null,
"ifd_style": "CLASSIC",
"personality": "MX",
"virtual": true
}
By running this script, you'll see a neatly formatted JSON output of the device's facts in your console.
Getting Interface Stats and Errors
You can use Predefined Junos PyEZ Operational Tables to fetch structured outputs. The Junos PyEZ jnpr.junos.op
module provides predefined Table and View definitions for RPCs corresponding to some common operational commands. For this example, I'm going to use the PhyPortErrorTable
For the most current list of Table and View definitions, see the Junos PyEZ GitHub repository at https://github.com/Juniper/py-junos-eznc/.
from jnpr.junos import Device
from jnpr.junos.op.phyport import PhyPortErrorTable
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
ports = PhyPortErrorTable(device)
output = ports.get()
for interface, errors in output.items():
print(f"Interface - {interface}")
print('---------------------')
for error in errors:
print(error)
print()
device.close()
- Connect to Device - Like before, we start by connecting to a device using its IP, username, and password.
- Fetch Interface Errors - We then instantiate
PhyPortErrorTable
from thejnpr.junos.op.phyport
module. This specific table is predefined within the Junos PyEZ library to fetch statistics and errors for physical interfaces. By callingports.get()
, we execute an underlying RPC call to retrieve detailed interface error statistics. - Iterate and Print - The script iterates through each interface returned by
PhyPortErrorTable
, printing out a list of error statistics for each. These include bytes and packets transmitted and received, as well as various error counts such as drops, discards, and collisions.
Interface - ge-0/0/0
---------------------
('rx_bytes', 0)
('rx_packets', 0)
('tx_bytes', 0)
('tx_packets', 0)
('rx_err_input', 0)
('rx_err_drops', 0)
('rx_err_frame', 0)
('rx_err_runts', 0)
('rx_err_discards', 0)
('rx_err_l3-incompletes', 0)
('rx_err_l2-channel', 0)
('rx_err_l2-mismatch', 0)
('rx_err_fifo', 0)
('rx_err_resource', 0)
('tx_err_carrier-transitions', 2)
('tx_err_output', 0)
('tx_err_collisions', 0)
('tx_err_drops', 0)
('tx_err_aged', 0)
('tx_err_mtu', 0)
('tx_err_hs-crc', 0)
('tx_err_fifo', 0)
('tx_err_resource', 0)
Interface - ge-0/0/1
---------------------
('rx_bytes', 0)
('rx_packets', 0)
('tx_bytes', 0)
('tx_packets', 0)
('rx_err_input', 0)
('rx_err_drops', 0)
('rx_err_frame', 0)
('rx_err_runts', 0)
('rx_err_discards', 0)
('rx_err_l3-incompletes', 0)
('rx_err_l2-channel', 0)
('rx_err_l2-mismatch', 0)
('rx_err_fifo', 0)
('rx_err_resource', 0)
('tx_err_carrier-transitions', 2)
('tx_err_output', 0)
('tx_err_collisions', 0)
('tx_err_drops', 0)
('tx_err_aged', 0)
('tx_err_mtu', 0)
('tx_err_hs-crc', 0)
('tx_err_fifo', 0)
('tx_err_resource', 0)
Interface - ge-0/0/2
---------------------
('rx_bytes', 0)
('rx_packets', 0)
('tx_bytes', 0)
('tx_packets', 0)
('rx_err_input', 0)
('rx_err_drops', 0)
('rx_err_frame', 0)
('rx_err_runts', 0)
('rx_err_discards', 0)
('rx_err_l3-incompletes', 0)
('rx_err_l2-channel', 0)
('rx_err_l2-mismatch', 0)
('rx_err_fifo', 0)
('rx_err_resource', 0)
('tx_err_carrier-transitions', 2)
('tx_err_output', 0)
('tx_err_collisions', 0)
('tx_err_drops', 0)
('tx_err_aged', 0)
('tx_err_mtu', 0)
('tx_err_hs-crc', 0)
('tx_err_fifo', 0)
('tx_err_resource', 0)
Interface - ge-0/0/3
---------------------
('rx_bytes', 0)
('rx_packets', 0)
('tx_bytes', 0)
('tx_packets', 0)
('rx_err_input', 0)
('rx_err_drops', 0)
('rx_err_frame', 0)
('rx_err_runts', 0)
('rx_err_discards', 0)
('rx_err_l3-incompletes', 0)
('rx_err_l2-channel', 0)
('rx_err_l2-mismatch', 0)
('rx_err_fifo', 0)
('rx_err_resource', 0)
('tx_err_carrier-transitions', 2)
('tx_err_output', 0)
('tx_err_collisions', 0)
('tx_err_drops', 0)
('tx_err_aged', 0)
('tx_err_mtu', 0)
('tx_err_hs-crc', 0)
('tx_err_fifo', 0)
('tx_err_resource', 0)
A Few Things to Note
Let's continue from where we left off and look into some of the specifics of the outputs you receive while working with PyEZ.
from jnpr.junos import Device
from jnpr.junos.op.phyport import PhyPortErrorTable
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
ports = PhyPortErrorTable(device)
output = ports.get()
print(type(output))
device.close()
<class 'jnpr.junos.factory.OpTable.PhyPortErrorTable'>
The output type <class 'jnpr.junos.factory.OpTable.PhyPortErrorTable'>
indicates that the output
variable is an instance of the PhyPortErrorTable
class, which is part of the Junos PyEZ library's operational table (OpTable) functionality. This class represents a predefined table structure that Junos PyEZ uses to fetch and represent operational data (in this case, physical port error statistics) from Junos devices.
What do we receive?
Keys and Values
In this section, we're diving deep into how data is structured when retrieved from a Junos device using the Junos PyEZ library and specifically, how to work with that data. After fetching physical port error statistics with the PhyPortErrorTable
, the output
variable holds this data in a structured format that represents the interface statistics.
The output.get()
method call returns a dictionary-like object where each key-value pair corresponds to an interface and its statistics.
from jnpr.junos import Device
from jnpr.junos.op.phyport import PhyPortErrorTable
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
ports = PhyPortErrorTable(device)
output = ports.get()
for interface, stats in output.items():
print(interface)
device.close()
- Keys - Each key in this dictionary-like object is the name of an interface on the device, such as
ge-0/0/0
,ge-0/0/1
, etc. - Values - Each value associated with a key is another object containing detailed statistics for that interface.
By iterating over output.items()
, we're effectively going through each interface and its associated statistics. However, in this specific script, we're only interested in demonstrating how to access and list the interface names, which are the keys in our data structure.
#output
ge-0/0/0
ge-0/0/1
ge-0/0/2
ge-0/0/3
ge-0/0/4
ge-0/0/5
ge-0/0/6
ge-0/0/7
ge-0/0/8
ge-0/0/9
This loop prints out the name of each interface, illustrating how to navigate the structured data returned by the Junos PyEZ library. Now, let's try and look at the values of the output.
from jnpr.junos import Device
from jnpr.junos.op.phyport import PhyPortErrorTable
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
ports = PhyPortErrorTable(device)
output = ports.get()
for interface, stats in output.items():
print(stats)
break
device.close()
Here, we print the statistics (stats
) for the first interface encountered in the loop and then immediately exit the loop with a break
statement. The reason for using break
is to simplify our output, allowing us to focus on understanding the data structure for one interface before getting overwhelmed with the details of all interfaces on the device.
[('rx_bytes', 0),
('rx_packets', 0),
('tx_bytes', 0),
('tx_packets', 0),
('rx_err_input', 0),
('rx_err_drops', 0),
('rx_err_frame', 0),
('rx_err_runts', 0),
('rx_err_discards', 0),
('rx_err_l3-incompletes', 0),
('rx_err_l2-channel', 0),
('rx_err_l2-mismatch', 0),
('rx_err_fifo', 0),
('rx_err_resource', 0),
('tx_err_carrier-transitions', 2),
('tx_err_output', 0),
('tx_err_collisions', 0),
('tx_err_drops', 0),
('tx_err_aged', 0),
('tx_err_mtu', 0),
('tx_err_hs-crc', 0),
('tx_err_fifo', 0),
('tx_err_resource', 0)]
The output is a list of tuples, where each tuple contains two elements. (I hate tuples, why not just use a dictionary 🥲)
- The first element of each tuple is a string representing the name of a statistic, such as
'rx_bytes'
,'rx_packets'
, etc. - The second element is the value of that statistic for the interface, which in this case are all numeric values (integers).
For example, the tuple ('rx_bytes', 0)
tells us that the number of received bytes (rx_bytes
) is 0.
Specific Example
In this example, let's try and focus on extracting and printing the rx_err_input
error counter for all interfaces on the device. Here's how we're doing it and what each part of the script accomplishes.
from jnpr.junos import Device
from jnpr.junos.op.phyport import PhyPortErrorTable
import json
device = Device(
host='10.10.20.21',
user='admin',
password='cisco123'
)
device.open()
ports = PhyPortErrorTable(device)
output = ports.get()
for interface, stats in output.items():
for stat in stats:
if stat[0] == 'rx_err_input':
print(f" rx_err_input counter on {interface} is {stat[1]}")
device.close()
- Connecting to the Device - As usual, we start by connecting to our Junos device using its IP address, username, and password.
- Fetching Interface Statistics - We use the
PhyPortErrorTable
to fetch detailed statistics about physical ports on the device. This is stored in theoutput
variable. - Iterating Through the Statistics - We then loop through each interface's statistics in
output.items()
. Eachinterface
is a key in the dictionary-like object, andstats
is the value, which is a list of tuples containing statistic names and their values. - Filtering for
rx_err_input
- Inside this loop, we have another loop that goes through each tuple instats
. We check if the first element of the tuple (stat[0]
) is'rx_err_input'
, which indicates we've found the error counter we're interested in. - Printing the Counter - When we find a tuple for
rx_err_input
, we print the interface name (interface
) and the corresponding counter value (stat[1]
), which gives us the number of input errors on that interface.
#output
rx_err_input counter on ge-0/0/0 is 0
rx_err_input counter on ge-0/0/1 is 0
rx_err_input counter on ge-0/0/2 is 0
rx_err_input counter on ge-0/0/3 is 0
rx_err_input counter on ge-0/0/4 is 0
rx_err_input counter on ge-0/0/5 is 0
rx_err_input counter on ge-0/0/6 is 0
rx_err_input counter on ge-0/0/7 is 0
rx_err_input counter on ge-0/0/8 is 0
rx_err_input counter on ge-0/0/9 is 0
Closing Thoughts
You must be wondering what the heck is a 'dictionary-like object'. When I refer to a "dictionary-like object," I'm talking about an object that behaves similarly to a Python dictionary but might not technically be a dict
type. In Python, a dictionary is a built-in data type that stores data in key-value pairs.
A "dictionary-like object" supports some or all of the operations you can perform on a standard Python dictionary, such as accessing values by keys, iterating over items, and checking for the presence of keys. However, it might come from a class that extends or mimics the dictionary behaviour for specific use cases, adding custom methods or changing how certain operations work.
In the context of the Junos PyEZ library, when we fetch data from a Junos device (like with the PhyPortErrorTable
), the returned object allows us to use key-value access and iteration, much like a standard dictionary.