Lab-12:Using and Creating PyEZ tables

Parsing command response is a challenge when automating network elements. It was a big challenge when interfaces were cli due to complexity and variation in cli implementation among vendors. NETCONF together with Python xml libraries made life easier.  I wrote two lab-7 & lab-8 on how to parse command response  in Python. They are written for NETCONF xml response  using minidom and etree, xpath.

PyEZ table and view abstract xml parsing from user.  Under the hood it does xml parsing in xpath but as a user you don’t need to worry about it. A user simply provides the command and  the response fields they are interested in, PyEZ does the parsing and adds it in Python dictionary.  User data is formatted in yaml.
In this lab I will demonstrate below functions of PyEZ
1) How to use existing PyEZ tables. PyEZ comes with lots of pre-built tables check references links for latest tables
2) How to quickly create your own tables & views on the fly in the Python script
3) how to build external table and use it in Python script

Below construction of table & view

table name - user defined table name
rpc - rpc command, for example get-interface-information. you can get rpc command 
for a cli like this 'show interfaces | display xml rpc' 
args - default optional argument for the rpc command
args_key - optional key for the table. if this key is not set you need to specify
key name for get() like this portStatsTable(dev).get(interface_name'ge-0/0/1'). If 
key is set you can run get() like this portStatsTable(dev).get('ge-0/0/1')
item - This is the top level xpath expression in <rpc-reply>. PyEZ searches for this 
string when parsing command response
view - view table name

fields - key value pair of dictionary. Here you specify response fields you are 
interested in, you also need to create key for that field

 

Using pre-built tables

Let’s see how to use pre-built PyEZ table. I am interested in interface error stats so I will try PhyPortErrorTable. This table contains input,output received counts and input,output error counts.
Below is the snippet of yaml file for the table

PhyPortErrorTable:
  rpc: get-interface-information  
  args:                           
    extensive: True 
    interface_name: '[fgx]e*' 
  args_key: interface_name        
  item: physical-interface        
  view: PhyPortErrorView          

PhyPortErrorView:
  groups: 
    ts: traffic-statistics 
    rxerrs: input-error-list
    txerrs: output-error-list
 fields_ts:                        
    rx_bytes: { input-bytes: int }
    rx_packets: { input-packets: int }
 <truncated ....>
 fields_rxerrs:
    rx_err_input: { input-errors: int }
    rx_err_drops: { input-drops: int }
 <truncated .....>
fields_txerrs:
    tx_err_carrier-transitions: { carrier-transitions: int }
    tx_err_output: { output-errors: int }
<truncated ....>

Step-1: Start Python and import required packages

sjakhwal@rtxl3rld05:~$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device
>>> from jnpr.junos.utils.config import Config
>>> from lxml import etree
>>> dev = Device(host='192.168.122.35',user='root',passwd='juniper1').open()
>>> dev.facts['model']
'FIREFLY-PERIMETER'
>>>

Step-2: Import PyEZ table and get command response. PyEZ keeps command response in a Python dictionary format, it makes name field as key automatically. You can see that interface_names  are keys in the dictionary

>>>from jnpr.junos.op.phyport import PhyPortErrorTable
>>> from pprint import pprint as pp
>>> phyErrors = PhyPortErrorTable(dev).get()
>>> phyErrors.keys()
[‘ge-0/0/0’, ‘ge-0/0/1’, ‘ge-0/0/2’]
>>>
>>> pp (phyErrors[‘ge-0/0/1’].items())
[(‘tx_err_resource’, 0),
(‘rx_err_drops’, 0),
(‘rx_packets’, 562159),
(‘rx_err_l2-channel’, 0),
(‘tx_err_collisions’, 0),
(‘tx_err_drops’, 0),
(‘tx_err_fifo’, 0),
(‘rx_err_frame’, 0),
(‘rx_err_runts’, 0),
(‘tx_bytes’, 84),
(‘tx_err_output’, 0),
(‘tx_err_aged’, 0),
(‘tx_err_hs-crc’, 0),
(‘rx_err_l2-mismatch’, 0),
(‘tx_err_carrier-transitions’, 1),
(‘rx_bytes’, 29229986),
(‘rx_err_fifo’, 0),
(‘rx_err_resource’, 0),
(‘rx_err_l3-incompletes’, 0),
(‘tx_err_mtu’, 0),
(‘rx_err_discards’, 0),
(‘rx_err_input’, 0),
(‘tx_packets’, 2)]
>>>
>>> pp (phyErrors[‘ge-0/0/1’].rx_bytes)
29229986
>>>
>>> for key in phyErrors.keys():
…   for phyError in phyErrors[key].items():
…     pp (phyError)

(‘tx_err_resource’, 0)
(‘rx_err_drops’, 0)
(‘rx_packets’, 783248)
(‘rx_err_l2-channel’, 0)
(‘tx_err_collisions’, 0)
(‘tx_err_drops’, 0)
(‘tx_err_fifo’, 0)
(‘rx_err_frame’, 0)
(‘rx_err_runts’, 0)
….
<truncated>

creating table and views on the fly

Now lets create our own table and views in the Python script. I am using same table (phyPortErrorStatsTable) but updated it to parse only ‘traffic-statistics’ & ‘input-error-list’.  I have also reduced the number of error counts.
Note:Be extra cautious about yaml format, make sure all fields are aligned properly otherwise loading will fail.

Step-1: You need to load two new packages for this. Import packages and create YAML string

>>>from jnpr.junos.factory.factory_loader import FactoryLoader
>>>import yaml
>>> PortErrorStatsTableString = """
... ---
... customPortErrorStatsTable:
...   rpc: get-interface-information
...   args:
...     extensive: True
...     interface_name: '[fgx]e*'
...   args_key: interface_name
...   item: physical-interface
...   view: customPhyPortErrorView
...
... customPhyPortErrorView:
...   groups:
...     ts: traffic-statistics
...     rxerrs: input-error-list
...   fields_ts:
...     rx_packets: { input-packets: int }
...     tx_packets: { output-packets: int }
...   fields_rxerrs:
...     rx_err_drops: { input-drops: int }
...     rx_err_discards: { input-discards: int }
... """

Step-2: Load YAML string and get command response. Remaining steps are same as using pre-built tables

>> globals().update(FactoryLoader().load(yaml.load(PortErrorStatsTableString)))
>>>
>>> customErrorsTable = customPortErrorStatsTable(dev).get()
>>> print customErrorsTable
customPortErrorStatsTable:192.168.122.35: 3 items
>>> print customErrorsTable.keys()
['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2']
>>> pp (customErrorsTable.items())
[('ge-0/0/0',
  [('rx_err_drops', 0),
   ('rx_packets', 792394),
   ('tx_packets', 19432),
   ('rx_err_discards', 0)]),
 ('ge-0/0/1',
  [('rx_err_drops', 0),
   ('rx_packets', 571240),
   ('tx_packets', 2),
   ('rx_err_discards', 0)]),
 ('ge-0/0/2',
  [('rx_err_drops', 0),
   ('rx_packets', 0),
   ('tx_packets', 0),
   ('rx_err_discards', 0)])]
>>>
>>> print customErrorsTable['ge-0/0/2'].tx_packets
0
>>>

Using table and views from external file

okay so the last part is to create external table file.
create a sub-directory myPyezTable and create .yml and .py file using below steps

Step-1: create portStats.py and put these 4 statements in the file

from jnpr.junos.factory import loadyaml
from os.path import splitext
_YAML_ = splitext(__file__)[0] + '.yml
globals().update(loadyaml(_YAML_))

Step-2: create portStats.yml file and add your  yaml table in it. Note both yml and py file names are same

---
customPortErrorStatsTable:
  rpc: get-interface-information
  args:
    extensive: True
    interface_name: '[fgx]e*'
  args_key: interface_name
  item: physical-interface
  view: customPhyPortErrorView
customPhyPortErrorView:
  groups:
    ts: traffic-statistics
    rxerrs: input-error-list
  fields_ts:
    rx_packets: { input-packets: int }
    tx_packets: { output-packets: int }
  fields_rxerrs:
    rx_err_drops: { input-drops: int }
    rx_err_discards: { input-discards: int }

Step-3: add _init.py file in the sub-directory. This is an empty file

Step-4: import your table. Remaining steps are same as using pre-build PyEZ tables

>>> from myPyezTable.portStats import customPortErrorStatsTable
>>> portErrors = customPortErrorStatsTable(dev).get()
>>> pp (portErrors.items())
[('ge-0/0/0',
  [('rx_err_drops', 0),
   ('rx_packets', 793354),
   ('tx_packets', 19525),
   ('rx_err_discards', 0)]),
 ('ge-0/0/1',
  [('rx_err_drops', 0),
   ('rx_packets', 572104),
   ('tx_packets', 2),
   ('rx_err_discards', 0)]),
 ('ge-0/0/2',
  [('rx_err_drops', 0),
   ('rx_packets', 0),
   ('tx_packets', 0),
   ('rx_err_discards', 0)])]
>>> pp (portErrors.keys())
['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2']
>>>

View of my sub-directory & external files

sjakhwal@rtxl3rld05:~/myPyezTable$ pwd
/home/sjakhwal/myPyezTable
sjakhwal@rtxl3rld05:~/myPyezTable$ ls
__init__.py  __init__.pyc  portStats.py  portStats.pyc  portStats.yml
sjakhwal@rtxl3rld05:~/myPyezTable$ cat __init__.py
sjakhwal@rtxl3rld05:~/myPyezTable$ cat portStats.py
from jnpr.junos.factory import loadyaml
from os.path import splitext
_YAML_ = splitext(__file__)[0] + '.yml'
globals().update(loadyaml(_YAML_))
sjakhwal@rtxl3rld05:~/myPyezTable$ cat portStats.yml
---
customPortErrorStatsTable:
  rpc: get-interface-information
  args:
    extensive: True
    interface_name: '[fgx]e*'
  args_key: interface_name
  item: physical-interface
  view: customPhyPortErrorView

customPhyPortErrorView:
  groups:
    ts: traffic-statistics
    rxerrs: input-error-list
  fields_ts:
    rx_packets: { input-packets: int }
    tx_packets: { output-packets: int }
  fields_rxerrs:
    rx_err_drops: { input-drops: int }
    rx_err_discards: { input-discards: int }

 

References:

Junos PyEZ Developer Guide

 

Lab-11:Loading configuration data using Juniper PyEZ

In this lab I will demonstrate how to load configuration data on Juniper srx using PyEZ API.I will show three different ways of loading configuration data 1) junos xml format file and string 2) junos set format command and string 3) text format command and string. I am using junos pdiff function to validate the result. pdiff shows the difference between committed and candidate datastore

Precondition:

  • Juniper srx with NETCONF protocol enabled
    • set system services netconf ssh
  • Juniper PyEZ installed
    • pip install junos-eznc

Procedure:

I am using python version 2.7 command line.

Step-1: create below 3 files we will be loading them using load API

filename:config_data_xml.xml
<configuration>
     <interfaces>
        <interface>
          <name>ge-0/0/2</name>
             <unit>
               <name>0</name>
                 <family>
                    <inet>
                       <address>
                         <name>192.168.1.1/24</name>
                       </address>
        
             </inet>
                  </family>
               </unit>
         </interface>
    </interfaces>
</configuration>


 

filename:config_data_set.set
set interfaces ge-0/0/2 unit 0 family inet address 192.168.3.1/24

 

filename:config_data_text.txt
interfaces ge-0/0/2 {
                    unit 0 {
                        family inet {
                                address 192.168.2.1/24
                         }
                    }
 }

Step-2: start Python and import packages. install Juniper eznc if not already done so (see precondition for installation command)

sjakhwal@rtxl3rld05:~/scripts/junos_config$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from jnpr.junos import Device
>>> from jnpr.junos.utils.config import Config
>>> from lxml import etree

Step-3: Connect to Juniper srx,print facts and bind config to device

>>>dev = Device(host='192.168.122.35',user='root',passwd='juniper1') 
>>>dev.open() Device(192.168.122.35)
>>>dev.facts['model'] 'FIREFLY-PERIMETER'
>>>config = Config(dev)

Step-4: Set file path for xml file, load file and validate using pdiff API. pdiff print show that a new route has been added to candidate datastore

>>> config_file_path = "/home/sjakhwal/scripts/junos_config/config_data_xml.xml"
>>> print etree.tostring(config.load(path=config_file_path,merge=True,format='xml'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+           }
+       }
+   }

Step-5: Same as step-4 but this time using Junos set format file. Set file path for Junos set  file, load file and validate using pdiff API. pdiff print show that new route 192.168.3.1 has been added in candidate datastore

## load junos set format file
>>> config_file_path = "/home/sjakhwal/scripts/junos_config/config_data_set.set"
>>> print etree.tostring(config.load(path=config_file_path,merge=True,format='set'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+               address 192.168.3.1/24;
+           }
+       }
+   }

Step-6: Set file path for Junos text  file, load file and validate using pdiff API

>>>config_file_path = "/home/sjakhwal/scripts/junos_config/config_data_text.txt"
>>> print etree.tostring(config.load(path=config_file_path,merge=True,format='text'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+               address 192.168.3.1/24;
+               address 192.168.2.1/24;
+           }
+       }
+   }

Step-7: Loading configuration data using Python string. You can create configuration data on the fly using Python string and push it to device. Create junos xml format configuration data string and load it. pdiff print show that new route has been added under interface ge-0/0/3

> config_data_xml = """
...   <configuration>
      <interfaces>
...       <interfaces>
...          <interface>
...                     <name>ge-0/0/3</name>
...                     <unit>
...                         <name>0</name>
...                         <family>
...                             <inet>
...                                 <address>
...                                     <name>192.168.1.1/24</name>
...                                 </address>
...                             </inet>
                    </unit>
...                         </family>
...                     </unit>
...                 </interface>
...       </interfaces>
... </configuration>
>>> print etree.tostring(config.load(config_data_xml,formal='xml'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+               address 192.168.3.1/24;
+               address 192.168.2.1/24;
+           }
+       }
+   }
[edit interfaces ge-0/0/3 unit 0 family inet]
        address 10.0.0.2/24 { ... }
+       address 192.168.1.1/24;

Step-8: same step as above but this time create configuration string for junos ‘set’ format. Create Junos set command string, load it and validate using pdiff API

>>> config_data_set = """
...  set interfaces ge-0/0/3 unit 0 family inet address 192.168.2.1/24
... """
>>> print etree.tostring(config.load(config_data_set,formal='set'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+               address 192.168.3.1/24;
+               address 192.168.2.1/24;
+           }
+       }
+   }
[edit interfaces ge-0/0/3 unit 0 family inet]
        address 10.0.0.2/24 { ... }
+       address 192.168.1.1/24;
+       address 192.168.2.1/24;

Step-9: Create test format Junos string, load it and validate using pdiff

>> config_data_text = """
...  interfaces ge-0/0/3 {
...                     unit 0 {
...                         family inet {
...                                 address 192.168.4.1/24
...                          }
...                     }
...  }
... """>>> print etree.tostring(config.load(config_data_text,format='text'))
<load-configuration-results>
<ok/>
</load-configuration-results>
>>> config.pdiff()
[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           family inet {
+               address 192.168.1.1/24;
+               address 192.168.3.1/24;
+               address 192.168.2.1/24;
+           }
+       }
+   }
[edit interfaces ge-0/0/3 unit 0 family inet]
        address 10.0.0.2/24 { ... }
+       address 192.168.1.1/24;
+       address 192.168.2.1/24;
+       address 192.168.4.1/24;
To verify the syntax of the configuration without actually committing it, you can call
the commit_check() method in place of the commit() method.
>>>config.commit_check()

Note: I encountered RPCtimeout error on commit_check. NETCONF default RPC timeout is 30 sec.
after increasing it to 300 sec command  completed successfully
>>>dev.timeout= 300  //300 seconds
>>> config.commit_check()
True
>>>

References: