Pyangbind
A plugin for pyang that creates Python bindings for a YANG model.
Install / Use
/learn @robshakir/PyangbindREADME
[![#PyangBind][img-pyangbind]][pyangbind-docs]
[![PyPI][img-pypi]][pypi-project] [![PyPI - License][img-license]][license] [![PyPI - Python Version][img-pyversion]][pypi-project]
PyangBind is a plugin for [Pyang][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][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 <a name="getting-started"></a>
PyangBind is distributed through [PyPI][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 <a name="generating-classes"></a>
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 ("{}/plugin".format(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:
$PYBINDPLUGINis the location that was exported from the above command.binding.pyis the desired output file.doc/base-test.yangis 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 <a name="using-in-python"></a>
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-network-instance.yang becomes openconfig_network_instance as a module name.
Primarily, we need to generate a set of classes for the model(s) that we are interested in. The OpenConfig network instance module will be used as an example for this walkthrough.
The bindings can be simply generated by running the docs/example/oc-network-instance/generate_bindings.sh script. This script clones the openconfig/public repo 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:
# Using the binding file generated by the `generate_bindings.sh` script
# Note that CWD is the file containing the binding.py file.
# Alternatively, you can use sys.path.append to add the CWD to the PYTHONPATH
from binding import openconfig_network_instance
ocni = openconfig_network_instance()
Creating a Data Instance <a name="create-instance"></a>
At this point, the ocni object can be used to manipulate the YANG data tree that is expressed by the module.
A subset of openconfig-network-instance looks like the following tree:
module: openconfig-network-instance
+--rw network-instances
+--rw network-instance* [name]
+--rw name -> ../config/name
+--rw config
| +--rw name? string
...
+--rw protocols
+--rw protocol* [identifier name]
+--rw identifier -> ../config/identifier
+--rw name -> ../config/name
+--rw config
| +--rw identifier? identityref
| +--rw name? string
| +--rw enabled? boolean
| +--rw default-metric? uint32
+--ro state
| +--ro identifier? identityref
| +--ro name? string
| +--ro enabled? boolean
| +--ro default-metric? uint32
+--rw static-routes
| +--rw static* [prefix]
| +--rw prefix -> ../config/prefix
| +--rw config
| | +--rw prefix? inet:ip-prefix
| | +--rw set-tag? oc-pt:tag-type
| | +--rw description? string
...
To add an entry to the network-instance list, the add method of the network-instance object is used to create instance a. Similarly a protocol of type STATIC and name of DEFAULT is added. Finally, a static route is added:
ocni.network_instances.network_instance.add('a')
ocni.network_instances.network_instance['a'].protocols
ocni.network_instances.network_instance['a'].protocols.protocol.add(identifier='STATIC', name='DEFAULT')
rt = ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].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 protocols container, which is a member of the network-instances 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:
rt.config.set_tag = 42
The tag value can then be accessed directly via the rt object, or via the original ocni object (which both refer to the same object in memory):
# Retrieve the tag value
print(rt.config.set_tag)
# output: 42
# Retrieve the tag value through the original object
print(ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static["192.0.2.1/32"].config.set_tag)
# 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:
print(ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static["192.0.2.1/32"].get(filter=True))
# returns {'prefix': '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.
The next-hop element in this model is another list. This keyed data structure acts like a Python dictionary, and has the special method add to add items to it. YANG leaf-list types use the standard Python list append method to add items to it. Equally, a list can be iterated through using the same methods as a dictionary, for example, using items():
# Add a set of next_hops
for nhop in [(0, "192.168.0.1"), (1, "10.0.0.1")]:
nh = rt.next_hops.next_hop.add(nhop[0])
nh.config.next_hop = nhop[1]
# Iterate through the next-hops added
for index, nh in rt.next_hops.next_hop.items():
print("{}: {}".format(index, nh.config.next_hop))
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:
# Try and set an invalid tag type
try:
rt.config.set_tag = "INVALID-TAG"
except ValueError as m:
print("Cannot set tag: {}".format(m))
Serialising a Data Instance <a name="serialising"></a>
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 [RFC 7950][rfc7950] - 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 networ
