This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Node Development

Using the FreeCONF API to build your management interface

Nodes are used to bridge your application to the YANG modeled management interface. As a software engineer you decide how those nodes are designed. Each management request constructs the node heirarchy to navigate to the part of the application to perform the management operation. When the management operation is complete, the nodes are garbage collected. Parallel requests construct different set of nodes so you can keep state in your request logic.

Unlike gRPC or OpenAPI, FreeCONF was designed to allow you to work with your existing application data structures and functions, not separate, generated ones. One reason for the difference is RESTCONF let’s the client pick and choose the data they want returned. Generating full data structures only to be partially populated is inefficient at best. Luckily FreeCONF gives you many options for how to implement your nodes, from a blank slate, to reflection, to code generation to anywhere in between.

Each node starts from a root node registered with the Browser object. Each node controls how its child nodes are constructed and therefore you can mix node implementations to suit each part of your existing application’s design.

If you’re not sure where to start, typically a YANG model uses the similar naming for fields and data structures of the application so I would start with Reflect and Extend. If your code becomes too repetative, then you can start to look at strategies for generating code to replace parts of your current code. This phased approach is useful because you’ll know what code to generate and replace.

1 - Interface

Understanding the role of a node.

You are unlikely to be implementing this interface as a developer but this gives a great understanding of all the responsiblities of a node. Each of the node base struct will give you options to control certain aspects of these functions.

type Node interface {

	// Child is called when navigating, creating or deleting a "container" or "list".
	// The request fields contain the nature of the request.
	//
	// Params:
	//  ChildRequest - contains the nature (e.g. get, delete, new) and details of the
	//                 request (e.g. identity name of the container or list )
	//
	// Return:
	//   Node - Return nil entity does not exist otherwise node implementation of the underlying
	//          node requested
	Child(r ChildRequest) (child Node, err error)

	// Next is called for items in a list when navigating, creating or deleting items in the
	// "list".
	//
	// Params:
	//  ListRequest - contains the nature (e.g. get, delete, new) and details of the
	//                 request (e.g. key, row number)
	//
	// Return:
	//   Node - Return nil entity does not exist otherwise node implementation of the underlying
	//          node requested
	//   []val.Value - If a key was defined in YANG, AND the request is for the next item
	//           in the list, then you must also return the key expressed in val.Value array
	Next(r ListRequest) (next Node, key []val.Value, err error)

	// Field is called to read or write "leaf" or "leaf-list" items on container/list items or
	// even root module.
	//
	// Params:
	//   FieldRequest - identity of the field and the contains the nature (e.g. read or write)
	//             tip: if the YANG defines the field as config:false, you can assume the field is
	//             only called for reading and you can ignore the flag for read v.s. write
	//
	//   *ValueHandle - contains a single the "Val" that with either:
	//                   a.) contain the value to be set or
	//                   b.) be expected to be set to the requested value to be read
	Field(r FieldRequest, hnd *ValueHandle) error

	// Choose is only called when there is a need to know which single case between several "choice/case"
	// sets are currently true.  This is only called when reading.
	//
	// Params:
	//   Selection - current selection that is handling the choice investigation
	//   *Choice - name for the choice (there could be several in a given container)
	//
	// Return:
	//   *ChoiceCase - which case is currently valid (there can be only one)
	//
	Choose(sel Selection, choice *meta.Choice) (m *meta.ChoiceCase, err error)

	// BeginEdit is called simply to inform a node when it is about to be edited including deleting or creating.
	// While there is nothing required of nodes to do anything on this call, implementations might have
	// very important, internal operations they need to perform like obtaining write locks for example. It is
	// also an opportunity to reject the edit for whatever reason by returning an error.
	//
	// It important to know this is called on **every** parent node for any edit to any of their children.
	// This is known as "event bubbling" and can be helpful when parents are in a better position to handle
	// operations for children.  You can easily distinguish this situation from the NodeRequest parameter.
	// See https://github.com/freeconf/restconf/wiki/Edit-Node-Traversal for details on when this is called
	// in particular in the order
	//
	// Params:
	//  NodeRequest - contains the nature (e.g. get, delete, new)
	BeginEdit(r NodeRequest) error

	// EndEdit is called simply to inform a node after it has been edited including deleting or creating.
	// While there is nothing required of nodes to do anything on this call, implementations might have
	// very important, internal operations they need to perform like release write locks for example. It is
	// also an opportunity to apply changes to live application or persist data to permanent storage.
	//
	// It important to know this is called on **every** parent node for any edit to any of their children.
	// This is known as "event bubbling" and can be helpful when parents are in a better position to handle
	// operations for children.  You can easily distinguish this situation from the NodeRequest parameter.
	// See https://github.com/freeconf/restconf/wiki/Edit-Node-Traversal for details on when this is called
	// in particular in the order
	//
	// Params:
	//  NodeRequest - contains the nature (e.g. get, delete, new)
	EndEdit(r NodeRequest) error

	// Action(rpc/action) is called when caller wished to run a 'action' or 'rpc' definition.  Input can
	// be found in request if an input is defined.  Output only has to be returned for
	// definitions that declare an output.
	//
	// Params:
	//  ActionRequest - In addition to the identity name of the action or rpc, it may contains the "input"
	//        to the action if one was defined in the YANG.
	//
	// Return:
	//  Node - If there is an expected response from action ("output" defined in YANG) then this would be
	//        the Node implementation of that data.
	Action(r ActionRequest) (output Node, err error)

	// Notify(notification) is called when caller wish to subscribe to events from a node.

	// Notificationsare unique with respect to resources are allocated that survive the original
	// request so implementation should try not keep references to any more resources than neccessary
	// to ensure they do not reference objects that become stale or that impose large chunks of memory
	// unknowningly and unneccesarily.
	//
	// Params:
	//   NotifyRequest - In addition to the identity name of the notification, this contains the stream
	//        to write events to.
	//
	// Return:
	//   NotifyCloser - Function to called to stop streaming events. This will be called for example when
	//      client closes the event stream socket either naturally or because of a network disconnect.
	//
	Notify(r NotifyRequest) (NotifyCloser, error)

	// Peek give an opportunity for API callers to gain access to the objects behind a node.
	//
	// Params:
	//   Selection - current selection that is handling the peeking
	//   interface{} - Who (or under what context) is attempting to peek. This can be used by
	//         node implementation to decide what is returned or if anything at all is returned.
	//         It may also be ingored.  This is up to
	//
	// Return:
	//   interface{} - Can be anything.
	//
	// This can be considered a violation of abstraction so implementation is not gauranteed and
	// can vary w/o any compiler warnings.  This can be useful in unit testing but uses outside this
	// should be used judicously.
	Peek(sel Selection, consumer interface{}) interface{}

	// Context provides an opportunity to add/change values in the request context that is passed to
	// operations on this node and operations to children of this node.  FreeCONF does not put or
	// require anything in the context, it is meant as a way to make application or request specific
	// data available to nodes.
	//
	// A popular use of context is to store the user and or user roles making the request so
	// operations can authorize or log user operations.  See RESTCONF request filters for one
	// way to implement that.
	Context(sel Selection) context.Context
}

2 - Edit lifecycle

In-depth details on developing your applications using FreeCONF and understanding the sequence of callbacks when editing node structures.

This is an in-depth, advanced description about how Node implementations handle edits.

When initiating an edit on your application you might want to call custom functions to create data structures at the beginning of the edit, at the end or at some point along the way. You might also want to implement custom locking. This document will go thru your options using FreeCONF library.

type Node interface {

    // Hook before an edit is made. opportunity to reject potential edit or create locks
    BeginEdit(r NodeRequest) error

    // Called to navigate and process edits
    Child(r ChildRequest) (child Node, err error)
    Next(r ListRequest) (next Node, key []val.Value, err error)
    Field(r FieldRequest, hnd *ValueHandle) error


    // Hook after an edit is made. opportunity to trigger persistance, finalize edit
    // or free locks
    EndEdit(r NodeRequest) error
}

Edit: Delete a node

[Root]
    [Parent]
        [Target]    <-- Delete Target
            [Child]
                [Child-Child]
NodeBeginEditEnd Edit
Root3. EditRoot=false,New=false,Delete=false6. EditRoot=false,New=false,Delete=false
Parent2. EditRoot=false,New=false,Delete=false5. EditRoot=false,New=false,Delete=false
Target1. EditRoot=true,New=false,Delete=true4. EditRoot=true,New=false,Delete=true

NOTES:

  1. The [Child] and [Child-Child] nodes never get called when their parent is deleted
  2. Every single ancester node of [Target] would be called on every edit no matter how deep the tree was. This would mean you could implement a single global write lock on a [Root] for any write edit if that is what you needed. Or a single write to database of entire tree on any edit.

Edit:Create a new node thru a target node

[Root]
    [Parent]
        [Target]    <-- New Child-Child request passed to Target
            [Child]
                [Child-Child]
NodeBeginEditEnd Edit
Root3. EditRoot=false,New=false,Delete=false10. EditRoot=false,New=false,Delete=false
Parent2. EditRoot=false,New=false,Delete=false9. EditRoot=false,New=false,Delete=false
Target1. EditRoot=true,New=false,Delete=false8. EditRoot=true,New=false,Delete=false
Child4. EditRoot=true,New=false,Delete=false7. EditRoot=true,New=false,Delete=false
Child-Child5. EditRoot=true,New=true,Delete=false6. EditRoot=true,New=true,Delete=false

Edit: Edit leaf on a node

[Root]
    [Parent]
        [Target]    <-- Edit leaf property on Target
            [Child]
                [Child-Child]
NodeBeginEditEnd Edit
Root3. EditRoot=false,New=false,Delete=false6. EditRoot=false,New=false,Delete=false
Parent2. EditRoot=false,New=false,Delete=false5. EditRoot=false,New=false,Delete=false
Target1. EditRoot=true,New=false,Delete=false4. EditRoot=true,New=false,Delete=false