Backend API

As there can be various use-case scenarios for Jetconf, bindings to a user application are not part of Jetconf server itself, but instead they are implemented in a separate package, so called “Jetconf backend”.

The basic idea of Jetconf’s backend architecture is that every node of the YANG schema (i.e. container, list, leaf-list) can have a custom handler object assigned to it. When a specific event affecting this node occurs , like configuration data being rewritten or RESCONF operation is called, an appropriate member function of this node handler is invoked.

As there are some major differences between YANG configuration data, state data and RPCs, the architecture of corresponding node handlers in Jetconf also has to follow these differences.

Backend package architecture

Every backend package for Jetconf server has to provide implementation of following modules.

  • usr_conf_data_handlers (Handlers for configuration data)
  • usr_state_data_handlers (Handlers for state data)
  • usr_op_handlers (Handlers for RESTCONF operations - RPCs)
  • us_action_handlers (Handlers for RESTCONF actions - operation on node)
  • usr_datastore (Datastore initialization and save/load functions can be customized here)
  • usr_init (Jetconf initialization)

In addition to this, backend package can also contain any other resources if necessary. When you consider writing a custom backend, looking at the very basic demo package jukebox-jetconf is a good way to start.

Handler inheritance

Because some data models can be quite large, it would be difficult to manually assign handler objects to all schema nodes. Because of this, for configuration and state data handlers, Jetconf offers a feature called Handler inheritance.

If a node without its own handler is edited, Jetconf finds a nearest parent node which has the handler assigned and then it calls its replace or replace_item method. It’s up to backend developer’s decision where to place handler objects, a more fine-grained placement will usually mean better performance (less data rewriting), at the cost of more work.

usr_init

Useful for code that has to be executed on the startup or on the end of Jetconf backend.

def jc_startup():

    # execute code on startup

def jc_end():

    # execute code on end

usr_datastore

Basic usr_datastore module without any customization.

from jetconf.data import JsonDatastore

class UserDatastore(JsonDatastore):
    pass

Customizing load() and save() functions

from jetconf.data import JsonDatastore


class UserDatastore(JsonDatastore):

    def load(self):

        # load method can be customized here

    def save(self):

        # save method can be customized here

usr_conf_data_handlers

The main purpose of configuration data handlers is to project all changes performed on a particular data node, like creation, modification or deletion, to the user application.

A configuration node handler is implemented by creating a custom class which inherits from either ConfDataObjectHandler or ConfDataListHandler base class depending on the type of YANG node. The former must be used when implementing a handler for Container or Leaf data nodes, while the latter is used for list-like types, specifically List and Leaf-List.

ConfDataObjectHandler:

Attributes:

self.ds             # type: jetconf.data.BaseDatastore
                    # Can be used for accessing the datastore content from handler functions

self.schema_path    # type: str
                    # Contains the YANG schema path to which this handler object is registered (as string)

self.schema_node    # type: yangson.schemanode.SchemaNode
                    # Contains the YANG schema path to which this handler object is registered (parsed)

Arguments:

ii:         # type: yangson.instance.InstanceRoute
            # Contains parsed instance identifier of the data node. Useful for determining list keys if this data node is a child of some list node.
ch:         # type: jetconf.data.DataChange
            # Can be used for accessing additional edit information, like HTTP input data if needed

Handlers derived from this base class has to implement the following interface:

from jetconf.handler_base import ConfDataObjectHandler
from yangson.instance import InstanceRoute
from jetconf.data import BaseDatastore, DataChange


class MyConfDataHandler(ConfDataObjectHandler):
    def create(self, ii: InstanceRoute, ch: DataChange):

        # Called when a new node is created

    def replace(self, ii: InstanceRoute, ch: DataChange):

        # Called when the node is being rewritten by new data

    def delete(self, ii: InstanceRoute, ch: DataChange):

        # Called when the node is deleted

ConfDataListHandler:

Attributes:

self.ds             # type: jetconf.data.BaseDatastore
                    # Can be used for accessing the datastore content from handler functions

self.schema_path    # type: str
                    # Contains the YANG schema path to which this handler object is registered (as string)

self.schema_node    # type: yangson.schemanode.SchemaNode
                    # Contains the YANG schema path to which this handler object is registered (parsed)

Arguments:

ii:     # type: yangson.instance.InstanceRoute
        # Contains parsed instance identifier of the data node. Useful for determining list keys if this data node is a child of some list node.

ch:     # type: jetconf.data.DataChange
        # Can be used for accessing additional edit information, like HTTP input data if needed

Handlers derived from this base class has to implement the following interface:

from jetconf.handler_base import ConfDataListHandler
from yangson.instance import InstanceRoute
from jetconf.data import BaseDatastore, DataChange


class MyConfDataHandler(ConfDataListHandler):
    def create_item(self, ii: InstanceRoute, ch: DataChange):

        # Called when a new item is added to the list or leaf-list

    def replace_item(self, ii: InstanceRoute, ch: DataChange):

        # Called when specific list item is being rewritten

    def delete_item(self, ii: InstanceRoute, ch: DataChange):

        # Called when an item is being deleted from the list

Handler registration

Assignation of handler objects to the specific data nodes is done via registering them in jetconf.handler_list.CONF_DATA_HANDLES handler list. Every usr_conf_data_handlers backend module must implement the global function register_conf_handlers, where the instantiation and registration of handler objects is done. This function is called on Jetconf startup after datastore initialization and has the following signature.

def register_conf_handlers(ds: BaseDatastore):

    ds.handlers.conf.register(MyConfHandler(ds, "/ns:schema-path/to-desired-node"))

usr_state_data_handlers

YANG state data, in contrast to the configuration data, represents more of a current state of the backend application. This means that they are not actually stored in Jetconf’s datastore, but instead they has to be generated on the go. Generation of state data is the purpose of state data handlers.

A state data handler has to acquire actual state data from backend application and generate data content of the node where it’s assigned. The output data are formatted in Python’s representation of JSON (using lists, dicts etc.) and their structure must be compliant with the standardized JSON encoding of YANG data (RFC7951).

A state node handler is implemented by creating a custom class which inherits from either StateDataContainerHandler or StateDataListHandler, depending on the YANG node type. This is similar to he configuration data handlers.

StateDataContainerHandler

Attributes:

self.ds             # type: jetconf.data.BaseDatastore
                    # Can be used for accessing the datastore content from handler functions

self.data_model     # type: yangson.datamodel.DataModel
                    # Reference to the current data model object

self.sch_pth        # type: str
                    # YANG schema path to which this handler object is registered (as string)

self.schema_node    # type: yangson.schemanode.DataNode
                    # Reference to the Yangson schema node object
from yangson.instance import InstanceRoute
from jetconf.handler_base import StateDataContainerHandler
from jetconf.data import BaseDatastore

class MyStateDataHandler(StateDataContainerHandler):
    def generate_node(self, node_ii: InstanceRoute, username: str, staging: bool)

        # This method has to generate content of the state data node

        return generated_content

StateDataListHandler

Attributes:

self.ds             # type: jetconf.data.BaseDatastore
                    # Can be used for accessing the datastore content from handler functions

self.data_model     # type: yangson.datamodel.DataModel
                    # Reference to the current data model object

self.sch_pth        # type: str
                    # YANG schema path to which this handler object is registered (as string)

self.schema_node    # type: yangson.schemanode.DataNode
                    # Reference to the Yangson schema node object

Methods:

from yangson.instance import InstanceRoute
from jetconf.helpers import JsonNodeT
from jetconf.handler_base import StateDataListHandler
from jetconf.data import BaseDatastore

class MyStateDataHandler(StateDataListHandler):
    def generate_list(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:

        # This method has to generate entire list

        return generated_content

    def generate_list(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:

        # Generates only one specific item of the list. The list key(s) of the item which needs to be generated can be resolved by processing the instance identifier passed in 'node_ii' argument.

        return generated_content

Handler registration

Assignation of state data handler objects to the specific data nodes is done via registering them in jetconf.handler_list.STATE_DATA_HANDLERS handler list. This is similar to the configuration data. Every usr_state_data_handlers backend module must implement the global function register_state_handlers, where the instantiation and registration of handler objects is done. This function is called on Jetconf startup after datastore initialization and has the following signature:

def register_state_handlers(ds: BaseDatastore):

    ds.handlers.state.register(MyStateDataHandler(ds, "/ns:schema-path/to/state/node"))

usr_op_handlers

Handlers for RESTCONF operations.

Arguments:

input_args:        # type: JSON
                   # Operation input arguments with structure defined by YANG model

username:          # type: jetconf.data.BaseDatastore
                   # Name of the user who invoked the operation

An operation handlers are implemented by adding a custom method to the class OpHandlersContainer. Finally, this class is instantiated and its methods are assigned to specific operation names.

from yangson.instance import InstanceRoute
from jetconf.helpers import JsonNodeT
from jetconf.data import BaseDatastore

class OpHandlersContainer:
    def __init__(self, ds: BaseDatastore):
        self.ds = ds

    def my_op_handler(self, input_args: JsonNodeT, username: str) -> JsonNodeT:

        # RPC operation Body

        # Operation output data as defined by YANG data model
        # output is not mandatory
        return output_data

Handler registration

Every usr_op_handlers backend module must implement the global function register_op_handlers, where the class OpHandlersContainer is instantiated and its methods are tied to individual operations. This function with following signature is called on Jetconf startup after datastore initialization.

def register_op_handlers(ds: BaseDatastore):

    op_handlers_obj = OpHandlersContainer(ds)
    ds.handlers.op.register(op_handlers_obj.my_op_handler, "ns:operation")

us_action_handlers

Handlers for RESTCONF actions.

Arguments:

ii:     # type: yangson.instance.InstanceRoute
       # Contains parsed instance identifier of the data node. Useful for determining list keys if this data node is a child of some list node.

input_args:        # type: JSON
                   # Operation input arguments with structure defined by YANG model

username:          # type: jetconf.data.BaseDatastore
                   # Name of the user who invoked the operation

An action handlers are implemented by adding a custom method to the class ActionHandlersContainer. Finally, this class is instantiated and its methods are assigned to specific action names and node path.

from yangson.instance import InstanceRoute
from jetconf.helpers import JsonNodeT
from jetconf.data import BaseDatastore

class ActionHandlersContainer:
    def __init__(self, ds: BaseDatastore):
        self.ds = ds

    def my_action_handler(self, ii: InstanceRoute, input_args: JsonNodeT, username: str) -> JsonNodeT:

        # Action Body

        # Action output data as defined by YANG data model
        # output is not mandatory
        return output_data

Handler registration

Every usr_action_handlers backend module must implement the global function register_action_handlers, where the class ActionHandlersContainer is instantiated and its methods are tied to individual actions. This function with following signature is called on Jetconf startup after datastore initialization.

def register_action_handlers(ds: BaseDatastore):
    act_handlers_obj = ActionHandlersContainer(ds)
    ds.handlers.action.register(act_handlers_obj.my_action_handler, "/ns:schema-path/to/action/node")