|
|
# Jetconf 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.
|
|
|
|
|
|
|
|
|
## Jetconf 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)
|
|
|
usr_datastore (Datastore initialization and save/load functions can be customized here)
|
|
|
```
|
|
|
|
|
|
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 [jetconf_jukebox](https://gitlab.labs.nic.cz/jetconf/jukebox-jetconf/tree/master/jetconf_jukebox) is a good way to start.
|
|
|
|
|
|
|
|
|
## Configuration 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. All configuration data handlers are contained within `usr_conf_data_handlers` module of the backend package.
|
|
|
|
|
|
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)
|
|
|
|
|
|
Methods:
|
|
|
Handlers derived from this base class has to implement the following interface:
|
|
|
|
|
|
create(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when a new node is created
|
|
|
- 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
|
|
|
|
|
|
replace(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when the node is being rewritten by new data
|
|
|
- Arguments: (same as above)
|
|
|
|
|
|
delete(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when the node is deleted
|
|
|
- Arguments: (same as above)
|
|
|
```
|
|
|
|
|
|
```
|
|
|
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)
|
|
|
|
|
|
Methods:
|
|
|
Handlers derived from this base class has to implement the following interface:
|
|
|
|
|
|
create_item(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when a new item is added to the list or leaf-list
|
|
|
- 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
|
|
|
|
|
|
replace_item(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when specific list item is being rewritten
|
|
|
- Arguments: (same as above)
|
|
|
|
|
|
delete_item(self, ii: InstanceRoute, ch: DataChange):
|
|
|
- Called when an item is being deleted from the list
|
|
|
- Arguments: (same as above)
|
|
|
```
|
|
|
|
|
|
### 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, 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.
|
|
|
|
|
|
|
|
|
### 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):
|
|
|
- Arguments:
|
|
|
ds: type jetconf.data.BaseDatastore
|
|
|
- Reference to the current datastore
|
|
|
```
|
|
|
- Example (see [jetconf_jukebox](https://gitlab.labs.nic.cz/jetconf/jukebox-jetconf/tree/master/jetconf_jukebox) demo backend):
|
|
|
```
|
|
|
CONF_DATA_HANDLES.register(MyConfHandler(ds, "/ns:schema-path/to-desired-node"))
|
|
|
```
|
|
|
|
|
|
## 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 (see [RFC7951]). All state data handlers are contained within `usr_state_data_handlers` module.
|
|
|
|
|
|
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
|
|
|
|
|
|
Methods:
|
|
|
generate_node(self, node_ii: InstanceRoute, username: str, staging: bool)
|
|
|
- This method has to generate content of the state data node
|
|
|
```
|
|
|
|
|
|
```
|
|
|
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:
|
|
|
def generate_list(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
|
|
|
- This method has to generate entire list
|
|
|
|
|
|
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.
|
|
|
```
|
|
|
|
|
|
### Handler inheritance:
|
|
|
Similarly to the configuration data, handler inheritance is also used with state data. When a generator for requested state node is not found, Jetconf will find and call the handler for the nearest parent node. Then, it selects only a desired subnode from these data generated by the parent handler.
|
|
|
|
|
|
|
|
|
### Handler registration:
|
|
|
Assignation of state data handler objects to the specific data nodes is done via registering them in jetconf.handler_list.STATE_DATA_HANDLES 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):
|
|
|
- Arguments:
|
|
|
ds: type jetconf.data.BaseDatastore
|
|
|
- Reference to the current datastore
|
|
|
```
|
|
|
- Example (see [jetconf_jukebox](https://gitlab.labs.nic.cz/jetconf/jukebox-jetconf/tree/master/jetconf_jukebox) demo backend):
|
|
|
```
|
|
|
STATE_DATA_HANDLES.register(MyStateHandler(ds, "/ns:schema-path/to/state/node"))
|
|
|
```
|
|
|
|
|
|
## Handlers for RESTCONF operations
|
|
|
An operation handlers are implemented by adding a custom method to the class `OpHandlersContainer` inside `usr_op_handlers` module. Finally, this class is instantiated and its methods are assigned to specific operation names.
|
|
|
|
|
|
All handler methods must have the following signature:
|
|
|
|
|
|
```
|
|
|
def my_op_handler(self, input_args: JsonNodeT, username: str) -> JsonNodeT:
|
|
|
- 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
|
|
|
- Return value:
|
|
|
Operation output data as defined by YANG data model, in Python's representation of JSON (using lists, dicts etc.) and compliant with the standardized JSON encoding of YANG data (see [RFC7951]).
|
|
|
```
|
|
|
|
|
|
### Handler inheritance:
|
|
|
None
|
|
|
|
|
|
### 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):
|
|
|
- Arguments:
|
|
|
ds: type jetconf.data.BaseDatastore
|
|
|
- Reference to the current datastore
|
|
|
```
|
|
|
- Example (see [jetconf_jukebox](https://gitlab.labs.nic.cz/jetconf/jukebox-jetconf/tree/master/jetconf_jukebox) demo backend):
|
|
|
```
|
|
|
op_handlers_obj = OpHandlersContainer(ds)
|
|
|
OP_HANDLERS.register(op_handlers_obj.my_op_handler, "ns:do-something-operation")
|
|
|
``` |