Skip to content

robshakir/pyangbind

Repository files navigation

#PyangBind

PyPI PyPI - License PyPI - Python Version

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 ("{}/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:

  • $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-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

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-hops 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

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 - 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 RFC 7951 - 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:

from pyangbind.lib.serialise import pybindIETFXMLEncoder
# Dump the entire instance as RFC 7950 XML
print(pybindIETFXMLEncoder.serialise(ocni))

This outputs the following XML fragment:

<openconfig-network-instance xmlns="http://openconfig.net/yang/network-instance">
  <network-instances>
    <network-instance>
      <name>a</name>
      <protocols>
        <protocol>
          <identifier>STATIC</identifier>
          <name>DEFAULT</name>
          <static-routes>
            <static>
              <prefix>192.0.2.1/32</prefix>
              <config>
                <set-tag>42</set-tag>
              </config>
              <next-hops>
                <next-hop>
                  <index>0</index>
                  <config>
                    <next-hop>192.168.0.1</next-hop>
                  </config>
                </next-hop>
                <next-hop>
                  <index>1</index>
                  <config>
                    <next-hop>10.0.0.1</next-hop>
                  </config>
                </next-hop>
              </next-hops>
            </static>
          </static-routes>
        </protocol>
      </protocols>
    </network-instance>
  </network-instances>
</openconfig-network-instance>

Or, similarly, using OpenConfig-suggested JSON:

import pyangbind.lib.pybindJSON as pybindJSON
print(pybindJSON.dumps(ocni, indent=2))

This outputs the following JSON structured text:

{
  "network-instances": {
    "network-instance": {
      "a": {
        "name": "a",
        "protocols": {
          "protocol": {
            "STATIC DEFAULT": {
              "identifier": "STATIC",
              "name": "DEFAULT",
              "static-routes": {
                "static": {
                  "192.0.2.1/32": {
                    "prefix": "192.0.2.1/32",
                    "config": {
                      "set-tag": 42
                    },
                    "next-hops": {
                      "next-hop": {
                        "0": {
                          "index": "0",
                          "config": {
                            "next-hop": "192.168.0.1"
                          }
                        },
                        "1": {
                          "index": "1",
                          "config": {
                            "next-hop": "10.0.0.1"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Note here that the static list and all parents are represented as a JSON object (such that if this JSON is loaded elsewhere, a prefix can be referenced using obj['network-instances']['network-instance']['a']['protocols']['protocol']['STATIC DEFAULT']['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(ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'], mode='ietf', indent=2))

And the corresponding output:

{
  "openconfig-network-instance:identifier": "STATIC",
  "openconfig-network-instance:name": "DEFAULT",
  "openconfig-network-instance:static-routes": {
    "static": [
      {
        "prefix": "192.0.2.1/32",
        "config": {
          "set-tag": 42
        },
        "next-hops": {
          "next-hop": [
            {
              "index": "0",
              "config": {
                "next-hop": "192.168.0.1"
              }
            },
            {
              "index": "1",
              "config": {
                "next-hop": "10.0.0.1"
              }
            }
          ]
        }
      }
    ]
  }
}

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 ingest 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:

import binding
new_ocni = pybindJSON.load(os.path.join("json", "oc-ni.json"), binding, "openconfig_network_instance")

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

# Manipulate the data loaded
print("Current tag: %d" % new_ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static['192.0.2.1/32'].config.set_tag)
# Outputs: 'Current tag: 42'

new_ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static['192.0.2.1/32'].config.set_tag += 1
print("New tag: %d" % new_ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static['192.0.2.1/32'].config.set_tag)
# 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:

# Load JSON into an existing class structure
from pyangbind.lib.serialise import pybindJSONDecoder
import json

ietf_json = json.load(open(os.path.join("json", "oc-ni_ietf.json"), 'r'))
pybindJSONDecoder.load_ietf_json(ietf_json, None, None, obj=new_ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'])

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:

# Iterate through the classes - both the 192.0.2.1/32 prefix and 192.0.2.2/32
# prefix are now in the objects
for prefix, route in new_ocni.network_instances.network_instance['a'].protocols.protocol['STATIC DEFAULT'].static_routes.static.items():
  print("Prefix: {}, tag: {}".format(prefix, route.config.set_tag))

# Output:
#		Prefix: 192.0.2.2/32, tag: 256
#		Prefix: 192.0.2.1/32, tag: 42

Example Code

This worked example can be found in the docs/example/oc-network-instance directory.

Further Documentation

Further information as to the implementation and usage of PyangBind can be found in the docs/ directory -- the README provides a list of documents and examples container therein.

Licensing

Copyright 2015, Rob Shakir (rjs@rob.sh)
Modifications copyright, the Pyangbind contributors.

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

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.
  • Additional development efforts were supported by Jive Communications, Inc.
  • Current maintenance is supported by Google.
  • Key contributions have been made to this project by the following developers, and companies. Many thanks are extended to them:
    • GoDaddy, particularly Joey Wilhelm's herculean efforts to refactor test code to use the unittest framework.
    • David Barroso, who initiated efforts to address Python 3 compatibility, and a number of other enhancements.
  • Design, debugging, example code, and ideas have been contributed by:
    • Members of the OpenConfig working group.
    • The managability team at Juniper Networks.