How to Use the Requests Library for Python on Devices Running Junos OS
The Requests library for Python is available
on certain devices running Junos OS that support the Python extensions
package. You can use the requests
module
in Python scripts to send HTTP/1.1 requests. On devices running Junos
OS with Enhanced Automation, you can also use the requests
module in Python interactive mode. The Requests library provides
additional methods for supporting initial deployments as well as for
performing routine monitoring and configuration changes on devices
running Junos OS. For information about the requests
module and its functions, see the Requests documentation at http://docs.python-requests.org/.
Issuing Requests
You can use the requests
module in
onbox Python scripts to send HTTP/1.1 requests. To make a request,
import the module in your script, and call the function corresponding
to the desired request. The module supports HTTP GET
and POST
requests as well as HEAD
, DELETE
, and PUT
requests. The request returns a Response object containing the server’s response. By default, requests
are made using the default routing instance.
The Requests library can be used to execute RPCs on devices
running Junos OS that support the REST API service. The target device
must be configured with the appropriate statements at the [edit
system services rest]
hierarchy level to enable Junos OS commands
over HTTP or HTTPS using REST.
For example, the following op script performs a GET request
that executes the get-software-information
RPC on a remote device running Junos OS that has the REST API service
over HTTP configured on the default port (3000). The script prints
the response status code, and if the status code indicates success,
it prints the response content.
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') r = requests.get('http://198.51.100.1:3000/rpc/get-software-information', auth=(user, password)) print (r.status_code) if (r.status_code == requests.codes.ok): print (r.text)
user@host> op show-version.py Enter user password: 200 <software-information> <host-name>router1</host-name> <product-model>mx240</product-model> <product-name>mx240</product-name> <junos-version>18.3R1.8</junos-version> <package-information> <name>os-kernel</name> ...
To retrieve just the headers, you can send a simple HEAD
request.
r = requests.head('http://198.51.100.1:3000/rpc/get-software-information', auth=(user, password)) print (r.headers) print (r.headers['content-type'])
user@host> op request-headers.py Enter user password: {'Date': 'Tue, 02 Apr 2019 18:30:58 GMT', 'Connection': 'close', 'Content-Type': 'application/xml; charset=utf-8', 'Server': 'lighttpd/1.4.48'} application/xml; charset=utf-8
If a GET request requires additional parameters, you can either
include the params
argument and supply
a dictionary or a list of tuples or bytes to send in the query string,
or you can pass in key/value pairs as part of the URL. Similarly,
you can supply custom headers by including the headers
argument and a dictionary of HTTP headers.
The following request executes the get-interface-information
RPC with the terse option for the given interface and returns the
response in text format:
headers={'content-type': 'application/xml', 'Accept': 'text/plain'} params={'interface-name':'ge-2/0/1', 'terse':''} r = requests.get('http://198.51.100.1:3000/rpc/get-interface-information', auth=(user, password), headers=headers, params=params)
The following example supplies the arguments as key/value pairs in the URL:
headers={'content-type': 'application/xml', 'Accept': 'text/plain'} rpc = 'get-interface-information?interface-name=ge-2/0/1&terse=' r = requests.get('http://198.51.100.1:3000/rpc/' + rpc, auth=(user, password), headers=headers)
To execute multiple RPCs in the same request, initiate an HTTP
POST request, and set the data
parameter
to reference the RPCs to execute. See sections Executing Operational RPCs and Managing the Configuration for examples that execute
multiple RPCs.
Executing Operational RPCs
You can use the requests
module to execute RPCs from the Junos
XML API on a remote device running Junos OS that has the REST API service
enabled.
The following op script uses the requests
module to execute the
RPC equivalent of the show interfaces ge-2/0/1 terse
operational mode command on the target device:
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') r = requests.get('http://198.51.100.1:3000/rpc/get-interface-information', auth=(user, password), params={'interface-name':'ge-2/0/1','terse':''}) print(r.text)
The following op script sends a POST request that executes multiple RPCs on the
target device. The data
parameter references the RPCs to
execute, which are defined in a multiline string for readability.
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') headers={'content-type': 'application/xml', 'Accept': 'text/plain'} payload=""" <get-software-information/> <get-interface-information> <interface-name>ge-2/0/1</interface-name> </get-interface-information>""" r = requests.post('http://198.51.100.1/rpc/', auth=(user, password), headers=headers, data=payload) if (r.status_code == requests.codes.ok): print (r.text)
You can also create a generic op script for which the user supplies the necessary
variables and the script constructs and executes the request. Consider the
following op script configuration, which configures the host
,
rpc
, and rpc_args
command line arguments
for the requests-rpc.py op script:
[edit system scripts] op { file requests-rpc.py { arguments { host { description "host:port to which to connect"; } rpc { description "base RPC to execute on target device"; } rpc_args { description "dictionary of RPC arguments to use"; } } } } language python;
The following sample op script connects to a remote device running Junos OS,
which has been configured with the appropriate statements at the [edit
system services rest]
hierarchy level to enable Junos OS commands
over HTTP using REST. The script prompts for the connection password and
connects to the host and port provided through the host
argument. The script then uses the requests
module to send a
GET request executing the RPC that was provided through the command-line
arguments.
Starting in Junos OS Release 21.2R1 and Junos OS Evolved Release 21.2R1, when the device passes command-line arguments to a Python op script, it prefixes a single hyphen (-) to single-character argument names and prefixes two hyphens (--) to multi-character argument names. In earlier releases, the devices prefixes a single hyphen (-) to all argument names.
# Junos OS Release 21.1 and earlier from junos import Junos_Context from ast import literal_eval import jcs import argparse import requests ## Argument list as configured in [edit system scripts op] arguments = { 'host': 'host:port to which to connect', 'rpc' : 'base RPC to execute on target device', 'rpc_args' : 'dictionary of RPC arguments to use' } ## Retrieve script arguments (Junos OS Release 21.1 and earlier) parser = argparse.ArgumentParser(description='This is a demo script.') for key in arguments: if key == 'rpc_args': parser.add_argument(('-' + key), help=arguments[key]) else: parser.add_argument(('-' + key), required=True, help=arguments[key]) args = parser.parse_args() ## Convert rpc_args to a dictionary if args.rpc_args is not None: args.rpc_args = literal_eval(args.rpc_args) ## Retrieve username and prompt for password for connecting to target device user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') ## Execute RPC if args.rpc_args is None: r = requests.get('http://' + args.host + '/rpc/' + args.rpc, auth=(user, password)) else: r = requests.get('http://' + args.host + '/rpc/' + args.rpc, auth=(user, password), params=args.rpc_args) ## Print RPC contents if HTTP status code indicates success if (r.status_code == requests.codes.ok): print (r.text) else: print (r.status_code)
When you execute the script, it executes the RPC with the specified options on the remote device and prints the response to standard output.
user@host> op requests-rpc.py host 198.51.100.1:3000 rpc get-interface-information rpc_args {'interface-name':'ge-2/0/1','terse':''} Enter user password: <interface-information xmlns="http://xml.juniper.net/junos/18.3R1/junos-interface" xmlns:junos="http://xml.juniper.net/junos/*/junos" junos:style="terse"> <physical-interface> <name>ge-2/0/1</name> <admin-status>up</admin-status> <oper-status>up</oper-status> </physical-interface> </interface-information>
Managing the Configuration
You can use the requests
module to
retrieve or change the configuration on a device running Junos OS
that has the REST API service enabled.
The following op script retrieves the [edit system]
hierarchy from the candidate configuration using a POST request:
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') headers = { 'content-type' : 'application/xml' } payload = '<get-configuration><configuration><system/></configuration></get-configuration>' r = requests.post('http://198.51.100.1:3000/rpc/', auth=(user, password), data=payload, headers=headers) print (r.content)
HTTP POST requests also enable you to execute multiple RPCs in a single request, for example, to lock, load, commit, and unlock a configuration.
The following sample op script connects to the remote device and configures an address on the given interface. The lock, load, commit, and unlock operations are defined separately for readability, but the RPCs are concatenated in the request.
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') #### lock, load, commit, unlock configuration headers={'content-type': 'application/xml'} lock = '<lock><target><candidate/></target></lock>' load = """<edit-config> <target><candidate></candidate></target> <default-operation>merge</default-operation> <config> <configuration> <interfaces> <interface> <name>ge-2/0/1</name> <unit> <name>0</name> <family> <inet> <address> <name>192.0.2.1/24</name> </address> </inet> </family> </unit> </interface> </interfaces> </configuration> </config> <error-option>stop-on-error</error-option> </edit-config>""" commit = '<commit/>' unlock = '<unlock><target><candidate/></target></unlock>' payload = lock + load + commit + unlock r = requests.post('http://198.51.100.1:3000/rpc/', auth=(user, password), headers=headers, data=payload) print(r.content)
When you execute the op script, it returns the RPC results
for the lock, load, commit, and unlock operations. On some devices,
the response output separates the individual RPC replies with boundary
lines that include --
followed by a boundary
string and a Content-Type
header. Other
devices might include just the Content-Type
header.
user@host> op requests-set-interface.py Enter user password: --harqgehabymwiax Content-Type: application/xml; charset=utf-8 <ok/> --harqgehabymwiax Content-Type: application/xml; charset=utf-8 <load-success/> --harqgehabymwiax Content-Type: application/xml; charset=utf-8 <commit-results xmlns:junos="http://xml.juniper.net/junos/*/junos"> <routing-engine junos:style="normal"> <name>re0</name> <commit-success/> <commit-revision-information> <new-db-revision>re0-1555351754-53</new-db-revision> <old-db-revision>re0-1555033614-52</old-db-revision> </commit-revision-information> </routing-engine> </commit-results> --harqgehabymwiax Content-Type: application/xml; charset=utf-8 <ok/> --harqgehabymwiax--
Using Certificates in HTTPS Requests
The HTTP basic authentication mechanism sends user credentials as a Base64-encoded clear-text string. To protect the authentication credentials from eavesdropping, we recommend enabling the RESTful API service over HTTPS, which encrypts the communication using Transport Layer Security (TLS) or Secure Sockets Layer (SSL). For information about configuring this service, see the Junos OS REST API Guide.
By default, the Requests library verifies SSL certificates for
HTTPS requests. You can include the verify
and cert
arguments in the request to
control the SSL verification options. For detailed information about
these options, see the Requests documentation.
When you use Python 2.7 to execute a script that uses
the requests
module to execute HTTPS requests,
the script generates InsecurePlatformWarning
and SubjectAltNameWarning
warnings.
The following op script sends a GET request over HTTPS,
and sets the verify
argument to the file
path of a CA bundle or a directory containing trusted CA certificates.
The specified CA certificates are used to verify the server’s
certificate.
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') r = requests.get('https://198.51.100.1:3443/rpc/get-software-information', auth=(user, password), verify='path-to-ca-bundle') print (r.status_code) if (r.status_code == requests.codes.ok): print (r.text)
To specify a local client-side certificate, set the cert
argument equal to the path of a single file containing
the client’s private key and certificate or to a tuple containing
the paths of the individual client certificate and private key files.
r = requests.get('https://198.51.100.1:3443/rpc/get-software-information', auth=(user, password), verify='path-to-ca-bundle', cert=('path-to-client-cert','path-to-client-key')
Specifying the Routing Instance
By default, requests are executed using the default routing
instance. You can also execute requests using the mgmt_junos
management instance or another non-default routing instance. When
you execute scripts through the Junos OS infrastructure, you can specify
the routing instance by calling the set_routing_instance()
function in the script. Certain devices also support specifying
the routing instance and executing a script in the Unix-level shell.
On devices running Junos OS Evolved, the set_routing_instance()
function only supports using
the management routing instance.
In a Python script, to execute a request using a non-default
routing instance, including the mgmt_junos
instance:
The following op script uses the mgmt_junos
management instance to connect to the target device and execute
requests.
from junos import Junos_Context import jcs import requests user = Junos_Context['user-context']['user'] password = jcs.get_secret('Enter user password: ') jcs.set_routing_instance('mgmt_junos') r = requests.get('http://198.51.100.1:3000/rpc/get-software-information', auth=(user, password)) print (r.text)
For information about using the set_routing_instance()
function in Python scripts, see set_routing_instance().
In addition to specifying the routing instance in the script,
certain devices support specifying the routing instance and executing
a script from the Unix-level shell. On devices running Junos OS with
Enhanced Automation (FreeBSD Release 7.1 or later), you can use the setfib
command to execute requests with the given routing instance,
including the management instance and other non-default routing instances.
The following Python script simply executes the get-software-information
RPC on a remote device and
prints the response:
#!/usr/bin/env python import requests from getpass import getpass user = raw_input ('Enter username: ') password = getpass('Enter password: ') r = requests.get('http://198.51.100.1:3000/rpc/get-software-information', auth=(user, password)) print (r.text)
To use setfib
to execute the script using
a non-default routing instance on a device running Junos OS with Enhanced
Automation:
Find the software index associated with the routing table for that instance.
In the following example, the device is configured to use the non-default dedicated management instance
mgmt_junos
. The routing table index is referenced in the command output.user@host> show route forwarding-table extensive table mgmt_junos Routing table: mgmt_junos.inet [Index 36738] Internet: Enabled protocols: Bridging, Destination: default Route type: permanent Route reference: 0 Route interface-index: 0 Multicast RPF nh index: 0 P2mpidx: 0 Flags: none Next-hop type: reject Index: 340 Reference: 1
To execute the op script with the given routing instance, use the
setfib
command to execute the script and reference the index. For example:user@host> start shell % setfib -F36738 python /var/db/scripts/op/request-software-info.py
In the following example, the device is configured with a non-default routing instance, vr1, and the vr1.inet routing table index is 8:
user@host> show route forwarding-table extensive table vr1 Routing table: vr1.inet [Index 8] Internet: Enabled protocols: Bridging, All VLANs, Destination: default Route type: permanent Route reference: 0 Route interface-index: 0 Multicast RPF nh index: 0 P2mpidx: 0 Flags: sent to PFE Next-hop type: reject Index: 592 Reference: 1
The following command executes the op script using the vr1 routing instance:
% setfib -F8 python /var/db/scripts/op/request-software-info.py
Performing ZTP Operations
Zero touch provisioning (ZTP) enables you to provision new Juniper Networks devices in your network automatically, with minimal manual intervention. To use ZTP, you configure a server to provide the required information, which can include a Junos OS image and a configuration file to load or a script to execute. When you physically connect a device to the network and boot it with a factory-default configuration, the device retrieves the information from the designated server, upgrades the Junos OS image as appropriate, and executes the script or loads the configuration file.
When you connect and boot a new networking device, if Junos OS detects a file on the server, the first line of the file is examined. If Junos OS finds the characters #! followed by an interpreter path, it treats the file as a script and executes it with the specified interpreter. You can use the Requests library in executed scripts to streamline the ZTP process.
For example, consider the following sample Python script,
which the new device downloads and executes during the ZTP process.
When the script executes, it first downloads the CA certificate from
the ca_cert_remote
location on the specified
server and stores it locally in the ca_cert_local
location. The script then connects to the configuration server on
port 8000 and issues a GET request to retrieve the new device configuration.
The request includes the path to the CA certificate, which is used
to verify the server’s certificate during the exchange. The
script then uses the Junos PyEZ library to load the configuration
on the device and commit it.
#!/usr/bin/python import os import paramiko import requests from jnpr.junos import Device from jnpr.junos.utils.config import Config # Define the servers storing the certificate and configuration host_cert = '198.51.100.1' host_config = '192.0.2.1' username = 'admin' password = 'secret123' # Define CA certificate file locations ca_cert_remote = '/u01/app/myCA/certs/rootCA.crt' ca_cert_local = '/var/tmp/rootCA.crt' # Retrieve the CA certificate from the server ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=host_cert, username=username, password=password) sftp = ssh.open_sftp() sftp.get(ca_cert_remote, ca_cert_local) sftp.close() ssh.close() # Retrieve the configuration from the server uri = 'https://' + host_config + ':8000/' config = requests.get(uri, auth=(username, password), verify=ca_cert_local) # Load and commit the configuration on the device with Device() as dev: cu = Config(dev) cu.load(config.text, format='text', overwrite=True) cu.commit()