NetDevOps

Getting Started With Juniper PyEZ Library

Getting Started With Juniper PyEZ Library
In: NetDevOps, Python, Juniper

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()
  1. Import Libraries - First, we import the necessary libraries. Device Class from jnpr.junos is used for connecting to and interacting with Junos devices. json is used for formatting the output in a readable manner.
  2. 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.
  3. Retrieve Device Facts - Once connected, we use the .facts attribute of the Device object to get a collection of facts about the device. This includes details like the device model, serial number, operating system version, and more.
  4. 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 use json.dumps with default=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 the facts dictionary can be serialized without errors.
  5. 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()
  1. Connect to Device - Like before, we start by connecting to a device using its IP, username, and password.
  2. Fetch Interface Errors - We then instantiate PhyPortErrorTable from the jnpr.junos.op.phyport module. This specific table is predefined within the Junos PyEZ library to fetch statistics and errors for physical interfaces. By calling ports.get(), we execute an underlying RPC call to retrieve detailed interface error statistics.
  3. 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()
  1. Connecting to the Device - As usual, we start by connecting to our Junos device using its IP address, username, and password.
  2. Fetching Interface Statistics - We use the PhyPortErrorTable to fetch detailed statistics about physical ports on the device. This is stored in the output variable.
  3. Iterating Through the Statistics - We then loop through each interface's statistics in output.items(). Each interface is a key in the dictionary-like object, and stats is the value, which is a list of tuples containing statistic names and their values.
  4. Filtering for rx_err_input - Inside this loop, we have another loop that goes through each tuple in stats. 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.
  5. 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.

References

https://www.juniper.net/documentation/us/en/software/junos-pyez/junos-pyez-developer/topics/concept/junos-pyez-tables-op-predefined.html

Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.