PyangBind is a plugin for Pyang that generates a Python class hierarchy from a YANG data model. The resulting classes can be directly interacted with in Python. Particularly, PyangBind will allow you to:

  • Create new data instances - through setting values in the Python class hierarchy.
  • Load data instances from external sources - taking input data from an external source and allowing it to be addressed through the Python classes.
  • Serialise populated objects into formats that can be stored, or sent to another system (e.g., a network element).

Development of PyangBind has been motivated by consuming the OpenConfig data models; and is intended to be well-tested against these models. The Python classes generated, and serialisation methods are intended to provide network operators with a starting point for loading data instances from network elements, manipulating them, and sending them to a network device. PyangBind classes also have functionality which allows additional methods to be associated with the classes, such that it can be used for the foundation of a NMS.


Contents


Getting Started

PyangBind is distributed through PyPI, it can be installed by simply running:

$ pip install pyangbind

The pyangbind module installs both the Pyang plugin (pyangbind.plugin.*), as well as a set of library modules (pyangbind.lib.*) that are used to provide the Python representation of YANG types.

Generating a Set of Classes

To generate your first set of classes, you will need a YANG module, and its dependencies. A number of simple modules can be found in the tests directory (e.g., tests/base-test.yang).

To generate a set of Python classes, Pyang needs to be provided a pointer to where PyangBind’s plugin is installed. This location can be found by running:

$  export PYBINDPLUGIN=`/usr/bin/env python -c \
	'import pyangbind; import os; print "%s/plugin" % os.path.dirname(pyangbind.__file__)'`
$ echo $PYBINDPLUGIN

Once this path is known, it can be provided to the --plugin-dir argument to Pyang. In the simplest form the command used is:

$ pyang --plugindir $PYBINDPLUGIN -f pybind -o binding.py tests/base-test.yang

where:

  • $PYBINDPLUGIN is the location that was exported from the above command.
  • binding.py is the desired output file.
  • doc/base-test.yang is the path to the YANG module that bindings are to be generated for.

There are a number of other options for PyangBind, which are discussed further in the docs/ directory.


Using the Classes in a Python Program

PyangBind generates a (set of) Python modules. The top level module is named after the YANG module - with the name made Python safe. In general this appends underscores to reserved names, and replaces hyphens with underscores - such that openconfig-local-routing.yang becomes openconfig_local_routing as a module name.

Primarily, we need to generate a set of classes for the model(s) that we are interested in. The OpenConfig local routing module will be used as an example for this walkthrough.

The bindings can be simply generated by running the doc/example/oc-local-routing/generate_bindings.sh script. This script simply uses curl to retrieve the modules, and subsequently, builds the bindings to a binding.py file as expressed above.

The simplest program using a PyangBind set of classes will look like:

1# Using the binding file generated by the `generate_bindings.sh` script
2# Note that CWD is the file containing the binding.py file.
3# Alternatively, you can use sys.path.append to add the CWD to the PYTHONPATH
4from binding import openconfig_local_routing
5
6oclr = openconfig_local_routing()

Creating a Data Instance

At this point, the oclr object can be used to manipulate the YANG data tree that is expressed by the module.

A subset of openconfig-local-routing looks like the following tree:

module: openconfig-local-routing
   +--rw local-routes
   ...
      +--rw static-routes
      |  +--rw static* [prefix]
      |     +--rw prefix    -> ../config/prefix
      |     +--rw config
      |     |  +--rw prefix?     inet:ip-prefix
      |     |  +--rw next-hop*   union

To add an entry to the static list the add method of the static object is used:

1rt = oclr.local_routes.static_routes.static.add("192.0.2.1/32")

The static list is addressed exactly as per the path that it has within the YANG module - such that it is a member of the static-routes container (whose name has been made Python-safe), which itself is a member of the local-routes container.

The add method returns a reference to the newly created list object - such that we can use the rt object to change characteristics of the newly created list entry. For example, a tag can be set on the route:

1rt.config.set_tag = 42

The tag value can then be accessed directly via the rt object, or via the original oclr object (which both refer to the same object in memory):

1# Retrieve the tag value
2print(rt.config.set_tag)
3# output: 42
4
5# Retrieve the tag value through the original object
6print(oclr.local_routes.static_routes.static["192.0.2.1/32"].config.set_tag)
7# output: 42

In addition, PyangBind classes which represent container or list objects have a special get() method. This dumps a dictionary representation of the object for printing or debug purposes (see the sections on serialisation for outputting data instances for interaction with other devices). For example:

1print(oclr.local_routes.static_routes.static["192.0.2.1/32"].get(filter=True))
2# returns {'prefix': u'192.0.2.1/32', 'config': {'set-tag': 42}}

The filter keyword allows only the elements within the class that have changed (are not empty or their default) to be output - rather than all possible elements.

Since the next-hop element in this model is a leaf-list this acts like a Python list, albeit one that validates the contents that are put into it. For example, the append method can be used to add entries to the list, which can be iterated through using a standard Python loop:

1# Add a next-hop
2rt.config.next_hop.append("10.0.0.1")
3rt.config.next_hop.append("192.168.207.1")
4
5# Iterate through the next-hops added
6for nh in rt.config.next_hop:
7  print("%d: %s" % (rt.config.next_hop.index(nh), nh))

Where (type or value) restrictions exist. PyangBind generated classes will result in a Python ValueError being raised. For example, if we attempt to set the set_tag leaf to an invalid value:

1# Try and set an invalid tag type
2try:
3  rt.config.set_tag = "INVALID-TAG"
4except ValueError as m:
5  print("Cannot set tag: %s" % m)

Serialising a Data Instance

Clearly, populated PyangBind classes are not useful in and of themselves - the common use case is that they are sent to a external system (e.g., a router, or other NMS component). To achieve this the class hierarchy needs to be serialised into a format that can be sent to the remote entity. There are currently multiple ways to do this:

  • XML - the rules for this mapping are defined in RFC6020bis - currently NOT SUPPORTED.
  • OpenConfig-suggested JSON - the rules for this mapping are currently being written into a formal specification. This is the standard (default) format used by PyangBind. Some network equipment vendors utilise this serialisation format.
  • IETF JSON - the rules for this mapping are defined in draft-ietf-netmod-yang-json - some network equipment vendors use this format.

Any PyangBind class can be serialised into any of the supported formats. Using the static route example above, the entire local-routing module can be serialised into OC-JSON using the following code:

1import pyangbind.lib.pybindJSON as pybindJSON
2# Dump the entire instance as JSON in PyangBind format
3print(pybindJSON.dumps(oclr))

This outputs the following JSON structured text:

 1{
 2    "local-routes": {
 3        "static-routes": {
 4            "static": {
 5                "192.0.2.1/32": {
 6                    "prefix": "192.0.2.1/32",
 7                    "config": {
 8                        "set-tag": 42,
 9                        "next-hop": [
10                            "10.0.0.1",
11                            "192.168.207.1"
12                        ]
13                    }
14                }
15            }
16        }
17    }
18}

Note here that the static list is represented as a JSON object (such that if this JSON is loaded elsewhere, a prefix can be referenced using obj["local-routes"]["static-routes"]["static"]["192.0.2.1/32"]).

It is also possible to serialise a subset of the data, e.g., only one list or container within the class hierarchy. This is done as follows (into IETF-JSON):

# Dump the static routes instance as JSON in IETF format
print(pybindJSON.dumps(oclr.local_routes, mode="ietf"))

And the corresponding output:

 1{
 2    "openconfig-local-routing:static-routes": {
 3        "static": [
 4            {
 5                "prefix": "192.0.2.1/32",
 6                "config": {
 7                    "set-tag": 42,
 8                    "next-hop": [
 9                        "10.0.0.1",
10                        "192.168.207.1"
11                    ]
12                }
13            }
14        ]
15    }
16}

Here, note that the list is represented as a JSON array, as per the IETF specification; and that only the static-routes children of the object have been serialised.

Deserialising a Data Instance

PyangBind also supports taking data instances from a remote system (or locally saved documents) and loading them into either a new, or existing set of classes. This is useful for when a remote system sends a data instance in response to a query - and the programmer wishes to injest this response such that further logic can be performed based on it.

Instances can be deserialised from any of the supported serialisation formats (see above) into the classes.

To de-serialise into a new object, the load method of the serialise module can be used:

1new_oclr = pybindJSON.load(os.path.join("json", "oc-lr.json"), binding, "openconfig_local_routing")

This creates a new instance of the openconfig_local_routing class that is within the binding module, and loads the data from json/oc-lr.json into it. The new_oclr object can then be manipulated as per any other class:

1# Manipulate the data loaded
2print("Current tag: %d" % new_oclr.local_routes.static_routes.static[u"192.0.2.1/32"].config.set_tag)
3# Outputs: 'Current tag: 42'
4
5new_oclr.local_routes.static_routes.static[u"192.0.2.1/32"].config.set_tag += 1
6print("New tag: %d" % new_oclr.local_routes.static_routes.static[u"192.0.2.1/32"].config.set_tag)
7# Outputs: 'Current tag: 43'

Equally, a JSON instance can be loaded into an existing set of classes - this is done by directly calling the relevant deserialisation class – in this case pybindJSONDecoder:

1# Load JSON into an existing class structure
2from pyangbind.lib.serialise import pybindJSONDecoder
3import json
4
5ietf_json = json.load(open(os.path.join("json", "oc-lr_ietf.json"), 'r'))
6pybindJSONDecoder.load_ietf_json(ietf_json, None, None, obj=new_oclr.local_routes)

The direct load_ietf_json method is handed a JSON object - and no longer requires the arguments for the module and class name (hence they are both set to None), rather the optional obj= argument is used to specify the object that corresponds to the JSON that is being loaded.

Following this load, the classes can be iterated through - showing both the original loaded route (192.0.2.1/32) and the one in the IETF JSON encoded file (192.0.2.2/32) exist in the data instance:

1# Iterate through the classes - both the 192.0.2.1/32 prefix and 192.0.2.2/32
2# prefix are now in the objects
3for prefix, route in new_oclr.local_routes.static_routes.static.iteritems():
4  print("Prefix: %s, tag: %d" % (prefix, route.config.set_tag))
5
6# Output:
7#                Prefix: 192.0.2.2/32, tag: 256
8#                Prefix: 192.0.2.1/32, tag: 43

Example Code

This worked example can be found in the docs/example/oc-local-routing directory on GitHub


Licensing

Copyright 2015, Rob Shakir (rjs@rob.sh)

This project has been supported by:
          * Jive Communications, Inc.
          * BT plc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Acknowledgements

  • This project was initiated as part of BT plc. Network Architecture ‘future network management’ projects.
  • Ongoing development and support for this project is supported by Jive Communications, Inc.
  • Members of the OpenConfig working group have assisted greatly in debugging, and designing a number of the approaches used in PyangBind.
  • Thanks to folks from Juniper Networks for their views on Python mapping, and examples that are included as part of the PyangBind unit tests.