Juniper's PyEZ

Loading Configuration Changes

Author: Kirk Byers
Date: 2015-03-03

Juniper has a Python library known as PyEZ which was created to simplify the programmatic management and control of Juniper devices.

You can find the PyEZ library here

This library relies upon the Junos API which uses NETCONF. NETCONF is a standardized protocol that facilitates the programmatic control and management of network devices. It was first published in December of 2006 in RFC4741 and was later revised in RFC6241.

NETCONF relies upon a combination of XML and RPC. It also uses existing underlying protocols for transport, encryption, and authentication. In Juniper's case NETCONF runs on top of SSH (NETCONF over SSH) using TCP port 830.

So given all of this—what do we need to do to use Juniper's NETCONF API? Basically, we need to do the following:

1. Install PyEZ: 

pip install junos-eznc  


2. Make sure your Junos device is configured to support NETCONF:

set system services netconf ssh


3. Have at it.

Note, for Juniper firewalls you also need to ensure that the NETCONF-SSH traffic is allowed into the device in "host-inbound-traffic > system-services".

For additional reference see:

Juniper's PyEZ Library Reference Documentation

Now that we have PyEZ installed, let's try to use it.

In my lab environment, I have a Juniper SRX100. I am connecting to this device from an AWS server running RedHat7 using Python 2.7.5.

My goal here is to use PyEZ and NETCONF to transfer a configuration snippet to the SRX

First, let's make the connection using PyEZ:

>>> from jnpr.junos import Device
>>> from jnpr.junos.utils.config import Config
>>> 
>>> a_device = Device(host='10.10.10.10', user='pyclass', password='********')
>>> a_device.open()
Device(10.10.10.10)

Now, let's print the device's facts:

>>> from pprint import pprint
>>> pprint(a_device.facts)
{'2RE': False,
 'HOME': '/cf/var/home/pyclass',
 'RE0': {'last_reboot_reason': '0x1:power cycle/failure',
         'model': 'RE-SRX100H2',
         'status': 'OK',
         'up_time': '52 days, 2 hours, 30 minutes, 39 seconds'},
 'domain': None,
 'fqdn': 'pynet-jnpr-srx1',
 'hostname': 'pynet-jnpr-srx1',
 'ifd_style': 'CLASSIC',
 'model': 'SRX100H2',
 'personality': 'SRX_BRANCH',
 'serialnumber': 'BZ0000AF0000',
 'srx_cluster': False,
 'switch_style': 'VLAN',
 'version': '12.1X44-D35.5',
 'version_info': junos.version_info(major=(12, 1), type=X, minor=(44, 'D', 35), build=5)}

So, we are in business—we have connected to the device and have retrieved a set of facts from it.

Now in order to transfer a configuration snippet, I need to use the load() method. Consequently, I create an instance of the Config class:

>>> cfg = Config(a_device)

Then I call the .load() method on this object. Let's try it...

I have this small configuration snippet which adds a static route:

$ cat test_config.conf 

routing-options {
    static {
        route 1.1.6.0/24 next-hop 10.220.88.1;
    }
}

You can see on the Juniper SRX that there are no pending changes:

pyclass@pynet-jnpr-srx1# show | compare 

[edit]
pyclass@pynet-jnpr-srx1#

Now we call the load method:

>>> cfg.load(path="test_config.conf", format="text", merge=True)
<Element load-configuration-results at 0x137c878>

The filename is 'test_config.conf' which is located in the current working directory. The file-format is 'text' which means it is in Juniper's curly-brace style (the other formats are 'set' and 'xml'). Finally, this is a merge action (the default action is a replace—see below for additional details on this).

Now let's check the SRX:

pyclass@pynet-jnpr-srx1# show | compare 
[edit routing-options static]
     route 1.1.5.0/24 { ... }
+    route 1.1.6.0/24 next-hop 10.220.88.1;

[edit]
pyclass@pynet-jnpr-srx1#

There is our pending change.

To reset the pending changes, I then perform a rollback.

pyclass@pynet-jnpr-srx1# rollback 0 
load complete

[edit]
pyclass@pynet-jnpr-srx1# show | compare    

[edit]
pyclass@pynet-jnpr-srx1#

Now, let's briefly look at what happens when merge=False; merge=False is the default and implies a replace action. A replace action, however, requires that the configuration file have a 'replace:' statement in it (otherwise PyEZ's load() method just performs a merge). Note, this is both what Juniper stated and what I observed in testing.

Consequently, I need to modify the test_config.conf file as follows:

$ cat test_config.conf 

routing-options {
    replace:
    static {
        route 1.1.6.0/24 next-hop 10.220.88.1;
    }
}

Then I re-execute the load() method and check the SRX.

>>> cfg.load(path="test_config.conf", format="text", merge=False)
<Element load-configuration-results at 0x137e638>

I now see that the entire static route section will be replaced upon commit.

pyclass@pynet-jnpr-srx1# show | compare    
[edit routing-options static]
+    route 1.1.6.0/24 next-hop 10.220.88.1;
-    route 0.0.0.0/0 next-hop 10.220.88.1;
-    route 1.1.1.0/24 next-hop 10.220.88.1;
-    route 1.1.2.0/24 next-hop 10.220.88.1;
-    route 1.1.3.0/24 next-hop 10.220.88.1;
-    route 1.1.4.0/24 next-hop 10.220.88.1;
-    route 1.1.5.0/24 next-hop 10.220.88.1;

[edit]
pyclass@pynet-jnpr-srx1#

We are now able to load configuration snippets into Junos using PyEZ and the NETCONF API. In the next article, I expand upon this to include locking the candidate config file, committing changes, and executing rollbacks.

Kirk Byers

@kirkbyers

You might also be interested in: