Juniper's PyEZ - Loading Configuration Changes

By Kirk Byers
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 at: https://github.com/Juniper/py-junos-eznc

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  

Note, there are several dependencies that you might need to resolve.


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

set system services netconf ssh


3. Have at it. 

For additional reference see:

Juniper's PyEZ library reference documentation

PyEZ basic connection example



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.


If you want to learn more about network automation, Python, and Ansible, join my email-list or follow me on Twitter: @kirkbyers. I also periodically run a free Python for Network Engineers email course which you can sign-up for here.

Kirk Byers
CCIE #6243 emeritus
Twitter: @kirkbyers

Juniper's PyEZ - Loading Configuration Changes

By Kirk Byers
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 at: https://github.com/Juniper/py-junos-eznc

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  

Note, there are several dependencies that you might need to resolve.


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

set system services netconf ssh


3. Have at it. 

For additional reference see:

Juniper's PyEZ library reference documentation

PyEZ basic connection example



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.


If you want to learn more about network automation, Python, and Ansible, join my email-list or follow me on Twitter: @kirkbyers. I also periodically run a free Python for Network Engineers email course which you can sign-up for here.

Kirk Byers
CCIE #6243 emeritus
Twitter: @kirkbyers