Example: Use a Custom YANG RPC to Retrieve Operational Information on Junos Devices
You can add YANG data models that define custom RPCs on Junos devices. Creating custom RPCs enables you to precisely define the input parameters and operations and the output fields and formatting for your specific operational tasks on those devices. This example presents a custom RPC and action script that retrieve operational information from the device and display customized CLI output.
The RPC is added to the Junos OS schema on the device. When the RPC is executed in the CLI, it prints the name and operational status for the requested physical interfaces.
Requirements
This example uses the following hardware and software components:
-
Device running Junos OS Release 17.3R1 or later that supports loading custom YANG data models.
Overview of the RPC and Action Script
The YANG module in this example defines a custom RPC to return the name and operational
status of certain physical interfaces. The YANG module rpc-interface-status
is saved in the rpc-interface-status.yang file. The module imports the
Junos OS extension modules, which provide the extensions required to execute custom RPCs on
the device and to customize the CLI output.
The module defines the get-interface-status
RPC. The
<get-interface-status>
request tag is used to remotely execute the
RPC on the device. In the RPC definition, the junos:command
statement
defines the command that is used to execute the RPC in the CLI, which in this case is
show intf status
.
The junos:action-execute
and junos:script
statements
define the action script that is invoked when the RPC is executed. This example uses a
Python action script named rpc-interface-status.py to retrieve the
information required by the RPC and return the XML output elements as defined in the RPC
output
statement.
rpc get-interface-status { description "RPC example to retrieve interface status"; junos:command "show intf status" { junos:action-execute { junos:script "rpc-interface-status.py"; } } ...
Starting in Junos OS Release 17.3, the action-execute
statement is a
substatement to command
. In earlier releases, the
action-execute
and command
statements are placed at
the same level, and the command
statement is optional.
The RPC has one input parameter named match
, which determines the
interfaces to include in the output. When you execute the RPC, you include a string that
matches on the desired interfaces, for example ge-0*. An empty string ("") matches on all
interfaces. The action script defines the default value for match
as an
empty string, so if the user omits this argument, the output will include information for
all interfaces.
input { leaf match { description "Requested interface match condition"; type string; } }
The RPC also defines the output nodes that must be emitted by the corresponding action
script. The root node is the <interface-status-info>
element, which
contains zero or more <status-info>
elements that enclose the
<interface>
and <status>
nodes for a matched
interface. The junos-odl:format interface-status-info-format
statement
defines the formatting for the output that is displayed in the CLI. This node is not emitted
in the output XML tree.
output { container interface-status-info { list status-info { leaf interface { type string; description "Physical inteface name"; } leaf status { type string; description "Operational status"; } junos-odl:format interface-status-info-format { ... } } } }
This example presents two versions of the Python action script. The scripts demonstrate
different means to retrieve the operational command output, but both scripts emit identical
RPC output. The first action script uses the Python subprocess
module to
execute the show interfaces match-value | display xml
command and then converts the string output into XML. The second action script uses Junos PyEZ to execute the RPC equivalent of the show
interfaces match-value
command. Both scripts use identical
code to parse the command output and extract the name and operational status for each
physical interface. The scripts construct the XML for the RPC output and then print the
output, which returns the information back to the device. The XML tree must exactly match
the hierarchy defined in the RPC.
Junos devices define release-dependent namespaces for many of the elements in the
operational output, including the <interface-information>
element.
In order to make the RPC Junos OS-release independent, the code uses the
local-name()
function in the XPath expressions for these elements. You
might choose to include the namespace mapping as an argument to xpath()
and qualify the elements with the appropriate namespace.
The module containing the RPC and the action script file are added to the device as part of
a new YANG package named intf-rpc
.
YANG Module
YANG Module
The YANG module, rpc-interface-status.yang, defines the RPC, the command used to execute the RPC in the CLI, and the name of the action script to invoke when the RPC is executed. The base name of the file must match the module name.
/* * Copyright (c) 2014 Juniper Networks, Inc. * All rights reserved. */ module rpc-interface-status { namespace "http://yang.juniper.net/examples/rpc-cli"; prefix rpc-cli; import junos-extension-odl { prefix junos-odl; } import junos-extension { prefix junos; } organization "Juniper Networks, Inc."; description "Junos OS YANG module for RPC example"; rpc get-interface-status { description "RPC example to retrieve interface status"; junos:command "show intf status" { junos:action-execute { junos:script "rpc-interface-status.py"; } } input { leaf match { description "Requested interface match condition"; type string; } } output { container interface-status-info { list status-info { leaf interface { type string; description "Physical interface name"; } leaf status { type string; description "Operational status"; } junos-odl:format interface-status-info-format { junos-odl:header "Physical Interface - Status\n"; junos-odl:indent 5; junos-odl:comma; junos-odl:space; junos-odl:line { junos-odl:field "interface"; junos-odl:field "status"; } } } } } } }
Action Script
The corresponding action script is rpc-interface-status.py. This
example presents two action scripts that use different means to retrieve the data. One
script uses the Python subprocess
module and the other script uses the
Junos PyEZ library. Both scripts emit the same RPC XML output.
Starting in Junos OS Release 21.2R1 and Junos OS Evolved Release 21.2R1, when the device passes command-line arguments to a Python action script, it prefixes a single hyphen (-) to single-character argument names and prefixes two hyphens (--) to multi-character argument names.
Action Script (Using subprocess
)
The following action script uses the Python subprocess
module to execute
the operational command and retrieve the data. This example provides two versions of the
script, which appropriately handle the script's command-line arguments for the different
releases.
Junos OS Release 21.1 and earlier
#!/usr/bin/python # Junos OS Release 21.1 and earlier import sys import subprocess from lxml import etree def get_device_info(cmd): """ Execute Junos OS operational command and parse output :param: str cmd: operational command to execute :returns: List containing the XML data for each interface """ # execute Junos OS operational command and retrieve output proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) tmp = proc.stdout.read() root = etree.fromstring(tmp.strip()) xml_items = [] # parse output for required data for intf in root.xpath("/rpc-reply \ /*[local-name()='interface-information'] \ /*[local-name()='physical-interface']"): # retrieve data for the interface name and operational status name = intf.xpath("*[local-name()='name']")[0].text oper_status = intf.xpath("*[local-name()='oper-status']")[0].text # append the XML for each interface to a list xml_item = etree.Element('status-info') interface = etree.SubElement(xml_item, 'interface') interface.text = name status = etree.SubElement(xml_item, 'status') status.text = oper_status xml_items.append(xml_item) return xml_items def generate_xml(cmd): """ Generate the XML tree for the RPC output :param: str cmd: operational command from which to retrieve data :returns: XML tree for the RPC output """ xml = etree.Element('interface-status-info') intf_list_xml = get_device_info(cmd) for intf in intf_list_xml: xml.append(intf) return xml def main(): args = {'match': ""} for arg in args.keys(): if arg in sys.argv: index = sys.argv.index(arg) args[arg] = sys.argv[index+1] # define the operational command from which to retrieve information cli_command = 'show interfaces ' + args['match'] + ' | display xml' cmd = ['cli', '-c', cli_command] # generate the XML for the RPC output rpc_output_xml = generate_xml(cmd) # print RPC output print (etree.tostring(rpc_output_xml, pretty_print=True, encoding='unicode')) if __name__ == '__main__': main()
Junos OS Release 21.2R1 and later
#!/usr/bin/python3 # Junos OS Release 21.2R1 and later import subprocess import argparse from lxml import etree def get_device_info(cmd): """ Execute Junos OS operational command and parse output :param: str cmd: operational command to execute :returns: List containing the XML data for each interface """ # execute Junos OS operational command and retrieve output proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) tmp = proc.stdout.read() root = etree.fromstring(tmp.strip()) xml_items = [] # parse output for required data for intf in root.xpath("/rpc-reply \ /*[local-name()='interface-information'] \ /*[local-name()='physical-interface']"): # retrieve data for the interface name and operational status name = intf.xpath("*[local-name()='name']")[0].text oper_status = intf.xpath("*[local-name()='oper-status']")[0].text # append the XML for each interface to a list xml_item = etree.Element('status-info') interface = etree.SubElement(xml_item, 'interface') interface.text = name status = etree.SubElement(xml_item, 'status') status.text = oper_status xml_items.append(xml_item) return xml_items def generate_xml(cmd): """ Generate the XML tree for the RPC output :param: str cmd: operational command from which to retrieve data :returns: XML tree for the RPC output """ xml = etree.Element('interface-status-info') intf_list_xml = get_device_info(cmd) for intf in intf_list_xml: xml.append(intf) return xml def main(): parser = argparse.ArgumentParser(description='This is a demo script.') parser.add_argument('--match', required=False, default='') parser.add_argument('--rpc_name', required=True) args = parser.parse_args() # define the operational command from which to retrieve information cli_command = 'show interfaces ' + args.match + ' | display xml' cmd = ['cli', '-c', cli_command] # generate the XML for the RPC output rpc_output_xml = generate_xml(cmd) # print RPC output print (etree.tostring(rpc_output_xml, pretty_print=True, encoding='unicode')) if __name__ == '__main__': main()
Action Script (Using Junos PyEZ)
The following action script uses Junos PyEZ to execute the operational command and retrieve the data. This example provides two versions of the script, which appropriately handle the script's command-line arguments for the different releases.
Junos OS Release 21.1 and earlier
#!/usr/bin/python # Junos OS Release 21.1 and earlier import sys from jnpr.junos import Device from jnpr.junos.exception import * from lxml import etree def get_device_info(match): """ Execute Junos OS operational command and parse output :param: str match: interface match condition :returns: List containing the XML data for each interface """ # execute Junos OS operational command and retrieve output try: with Device() as dev: if (match == ""): root = dev.rpc.get_interface_information( ) else: root = dev.rpc.get_interface_information(interface_name=match) except Exception: sys.exit() xml_items = [] # parse output for required data for intf in root.xpath("/rpc-reply \ /*[local-name()='interface-information'] \ /*[local-name()='physical-interface']"): # retrieve data for the interface name and operational status name = intf.xpath("*[local-name()='name']")[0].text oper_status = intf.xpath("*[local-name()='oper-status']")[0].text # append the XML for each interface to a list xml_item = etree.Element('status-info') interface = etree.SubElement(xml_item, 'interface') interface.text = name status = etree.SubElement(xml_item, 'status') status.text = oper_status xml_items.append(xml_item) return xml_items def generate_xml(match): """ Generate the XML tree for the RPC output :param: str match: interface match condition :returns: XML tree for the RPC output """ xml = etree.Element('interface-status-info') intf_list_xml = get_device_info(match) for intf in intf_list_xml: xml.append(intf) return xml def main(): args = {'match': ""} for arg in args.keys(): if arg in sys.argv: index = sys.argv.index(arg) args[arg] = sys.argv[index+1] # generate the XML for the RPC output rpc_output_xml = generate_xml(args['match']) # print RPC output print (etree.tostring(rpc_output_xml, pretty_print=True, encoding='unicode')) if __name__ == '__main__': main()
Junos OS Release 21.2R1 and later
#!/usr/bin/python3 # Junos OS Release 21.2R1 and later import sys import argparse from jnpr.junos import Device from jnpr.junos.exception import * from lxml import etree def get_device_info(match): """ Execute Junos OS operational command and parse output :param: str match: interface match condition :returns: List containing the XML data for each interface """ # execute Junos OS operational command and retrieve output try: with Device() as dev: if (match == ""): root = dev.rpc.get_interface_information( ) else: root = dev.rpc.get_interface_information(interface_name=match) except Exception: sys.exit() xml_items = [] # parse output for required data for intf in root.xpath("/rpc-reply \ /*[local-name()='interface-information'] \ /*[local-name()='physical-interface']"): # retrieve data for the interface name and operational status name = intf.xpath("*[local-name()='name']")[0].text oper_status = intf.xpath("*[local-name()='oper-status']")[0].text # append the XML for each interface to a list xml_item = etree.Element('status-info') interface = etree.SubElement(xml_item, 'interface') interface.text = name status = etree.SubElement(xml_item, 'status') status.text = oper_status xml_items.append(xml_item) return xml_items def generate_xml(match): """ Generate the XML tree for the RPC output :param: str match: interface match condition :returns: XML tree for the RPC output """ xml = etree.Element('interface-status-info') intf_list_xml = get_device_info(match) for intf in intf_list_xml: xml.append(intf) return xml def main(): parser = argparse.ArgumentParser(description='This is a demo script.') parser.add_argument('--match', required=False, default='') parser.add_argument('--rpc_name', required=True) args = parser.parse_args() # generate the XML for the RPC output rpc_output_xml = generate_xml(args.match) # print RPC output print (etree.tostring(rpc_output_xml, pretty_print=True, encoding='unicode')) if __name__ == '__main__': main()
Enabling the Execution of Python Scripts
To enable the device to execute unsigned Python scripts:
Loading the RPC on the Device
To add the RPC and action script to the Junos schema:
Verifying the RPC
Purpose
Verify that the RPC works as expected.
Action
From operational mode, execute the RPC in the CLI by
issuing the command defined by the junos:command
statement in the RPC definition, and include the match
input argument. In this example, the match argument is used to match
on all interfaces that start with ge-0.
user@host> show intf status match ge-0* Physical Interface - Status ge-0/0/0, up ge-0/0/1, up ge-0/0/2, up ge-0/0/3, up ge-0/0/4, up ge-0/0/5, up ge-0/0/6, up ge-0/0/7, up ge-0/0/8, up ge-0/0/9, up ge-0/1/0, up ge-0/1/1, up ge-0/1/2, up ge-0/1/3, up ge-0/1/4, up ge-0/1/5, up ge-0/1/6, up ge-0/1/7, up ge-0/1/8, up ge-0/1/9, up
You can also adjust the match condition to return different sets of interfaces. For example:
user@host> show intf status match *e-0/*/0 Physical Interface - Status ge-0/0/0, up pfe-0/0/0, up ge-0/1/0, up xe-0/2/0, up xe-0/3/0, up
To return the same output in XML format, append the | display xml
filter to the command.
user@host> show intf status match *e-0/*/0 | display xml <rpc-reply xmlns:junos="http://xml.juniper.net/junos/17.3R1/junos"> <interface-status-info> <status-info> <interface>ge-0/0/0</interface> <status>up</status> </status-info> <status-info> <interface>pfe-0/0/0</interface> <status>up</status> </status-info> <status-info> <interface>ge-0/1/0</interface> <status>up</status> </status-info> <status-info> <interface>xe-0/2/0</interface> <status>up</status> </status-info> <status-info> <interface>xe-0/3/0</interface> <status>up</status> </status-info> </interface-status-info> <cli> <banner></banner> </cli> </rpc-reply>
To match on all interfaces, either omit the match
argument or set the value of the argument to an empty string ("").
Meaning
When you execute the RPC, the device invokes the action script. The action script executes the
operational command to retrieve the interface information from the device, parses the
output for the desired information, and prints the XML hierarchy for the RPC output as
defined in the RPC output
statement. When you execute the RPC in the CLI,
the device uses the CLI formatting defined in the RPC to convert the XML output into the
displayed CLI output. To return the original XML output, append the | display
xml
filter to the command.
When the RPC is executed remotely using the RPC request tag, the default format for the output is XML.
Troubleshooting RPC Execution Errors
Problem
Description
When you execute the RPC, the device generates the following error:
error: open failed: /var/db/scripts/action/rpc-interface-status.py: Permission denied
Cause
The user who invoked the RPC does not have the necessary permissions to execute the corresponding Python action script.
Solution
Users can only execute unsigned Python scripts on Junos devices when the script's file permissions include read permission for the first class that the user falls within, in the order of user, group, or others.
Verify whether the script has the necessary permissions for that user to execute the script, and adjust the permissions, if appropriate. If you update the permissions, you must also update the YANG package in order for this change to take effect. For example:
admin@host> file list ~ detail -rw------- 1 admin wheel 2215 Apr 20 11:36 rpc-interface-status.py
admin@host> file change-permission rpc-interface-status.py permission 644 admin@host> file list ~ detail -rw-r--r-- 1 admin wheel 2215 Apr 20 11:36 rpc-interface-status.py
admin@host> request system yang update intf-rpc action-script /var/tmp/rpc-interface-status.py Scripts syntax validation : START Scripts syntax validation : SUCCESS
Change History Table
Feature support is determined by the platform and release you are using. Use Feature Explorer to determine if a feature is supported on your platform.