Internet-Draft | SID Extension | March 2024 |
Toutain, et al. | Expires 27 September 2024 | [Page] |
As the Internet of Things (IoT) systems are becoming more pervasive with their rapid adoption, they are also becoming more complex in their architecture. Hence, a tool is required to generate prototype code based on the YANG models for constrained devices [RFC7228] to improve interoperability and increase the reusability of software components. A novel approach is introduced in this document to generate software prototypes (also called stubs) in the C language for the CORECONF protocol [I-D.ietf-core-comi] using YANG Schema Item iDentifiers (YANG SID [I-D.ietf-core-sid]). These stubs greatly reduce the complexity of navigating the CORECONF structure by abstracting the corresponding YANG SIDs. This document elaborates on the procedure to generate YANG SIDs for a given YANG model of a system, which then generates C stubs using the tools developed by the authors with the help of an example (sensor.yang file).¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 27 September 2024.¶
Copyright (c) 2024 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
The YANG modeling language is very popular for node configuration and retrieving the the state of a device.
XML and JSON are also two popular formats to serialize data in conformance with the data model. Recently, a
new serialization format has
been published, allowing a more compact representation of the information. This new format is based on CBOR
and the YANG identifier; instead of being represented by a unique ASCII string, values are mapped to an unique integer.¶
The mapping between strings and integer is kept in a .sid file usually generated by tools such as pyang.¶
This document presents some extensions to the sid files that allow to ease the conversion between CBOR and JSON, as well as manipulating YANG Data Models in a constraint environment.¶
YANG is a modeling language to structure information and check its conformity. Each element of a YANG Data Model is identified through an unique identifier. YANG is based on a hierarchical approach. During the serialization phase, data is represented either in XML or JSON. In this document we will use the JSON representation. [RFC7951] indicates how to form a JSON structure conforming to a YANG Data Model:¶
Leaves are represented as JSON objects, the key being the leaf name.¶
Lists are defined through arrays.¶
Identityref is a string containing the identifier in the YANG naming hierarchy for an identity.¶
The YANG Data Model, in Figure 1, is used to illustrate how data will be serialized. It defines a container representing a physical object able to perform several measurements. The physical object is battery powered, has a status LED able to change color, and a list of attached sensors returning an integer value.¶
The YANG tree regarding this Data Model is given in Figure 2. The tree displays the module hierarchy. For the module "sensor", a container "sensorObject" contains two leaves ("statusLED", "battery") and a list ("sensorReadings").¶
An example of data, serialized with JSON, and conforming to the YANG Data Model, is given in Figure 3. Embedded JSON Objects allow to represent the hierarchy. The key of the outer JSON Object is composed of the module name and the container name. The embedded JSON Object associated to this contains the leaves as keys. The YANG list is represented by an JSON Array.¶
JSON notation is verbose for constrained networks and objects. To optimize the size of the representation, [RFC9254] defines a CBOR serialization, also used in CORECONF. YANG ASCII identifiers are replaced by unique number. In JSON, the uniqueness is guaranteed by the "namespace" URI, as shown in Figure 1. By construction, the rest of the identifiers are unique.¶
In CORECONF, the uniqueness is guaranteed through the use of positive integers called SID, which replace the ASCII identifiers. [I-D.ietf-core-sid] defines the allocation process. Module developers may ask for a SID range from their authority. For example, for an IETF module, the IANA will allocate a SID range.¶
The Figure 4 shows an example of this conversion. The range is arbitrarily fixed between 60000 and 60099. Note that the module, the identities, and the leaves have an assigned SID.¶
To perform the allocation, the pyang utility generates a .sid file in the JSON format, resulting in a YANG Data Model specified in [I-D.ietf-core-sid]. The Figure 5 gives an excerpt.¶
The serialization in CBOR of the JSON example in Figure 3 is given in Figure 6. Compared to the compacted representation of the JSON structure (152 Bytes), the CORECONF structure is 24 bytes long. Although the size is different, the structure remains the same. It is composed of embedded CBOR Maps (equivalent of JSON Object). The first key is a SID (60005 corresponds to the container). Embedded CBOR Maps use a delta notation to encode the keys. The key 5 corresponds to SID 60005+5, thus to the leaf "statusLED". Key 2 in the second CBOR Map corresponds to the YANG list "sensorReadings", and the elements of the list are stored in a CBOR Array.¶
Note that in this example, the enum for "statusLED" (60005+5) is an integer and the identityref for "battery" (60005+1) is also an integer pointing to the SID "med-level" (60004).¶
Even if the conversion between CBOR and JSON formats might look obvious, including data compatible with a YANG Data Model is not so trivial. The reason is that JSON uses ASCII identifiers for readability and CBOR prefers integers for compactness. The Table 1 summarizes some YANG types when coded in JSON ([RFC7951]) or CBOR ([RFC8949]).¶
YANG | JSON | CBOR |
---|---|---|
int8, int16, int32, uint8, uint16, uint32 | number | +int, -int |
int64, uint64 | string | +int, -int |
decimal64 | string | CBOR Tag 4 |
binary | base64 | byte array |
bits | string | array |
boolean | boolean | boolean |
identityref | string | +int |
enumeration | string | +int |
The conversion from CBOR to JSON is not direct, since the YANG type needs to be considered. For instance, a integer could be converted into a number, an enum, or an identityref. Note that for union, the conversion is simple since some CBOR Tags may be used to indicate how the integer is converted, but outside of the union, for a single value, there is no such clue.¶
On the other direction, a JSON string may correspond to a 64-bit long number, an enumeration, or an identityref.¶
To perform the conversion, the YANG type is needed, but popular tools such as yanglint or pyang are not currently manipulating CBOR representation. Furthermore, these tools cannot run on constrained devices. To overcome these problems, we propose to extend the .sid file with more information. The modified pyang code (see https://github.com/ltn22/pyang/tree/sid-extension) adds useful information to the .sid file when the "--sid-extension" argument is provided.¶
This extension contains a "type" key in the JSON Object describing the mapping between the SID and the identifier if the node is a leaf (see Figure 7).¶
The "type" key added to leaf nodes contains several information:¶
when the "type" key is not present, the node is a not leaf and there is no need for type conversion.¶
when the value of "type" is a string, it gives the YANG type of the leaf, as defined in the YANG Data Model. If the YANG type derives from an identityref, the value "identityref" is given instead of the YANG type specified in the module. In that case, in the CBOR notation, there is a pair "{ ...., SID1: value1, ...} in the CBOR Map. A first lookup at the .sid file for SID1 returns that the type is "identityref", a second lookup for value1 in the "identity" namespace, returns the ASCII identifier.¶
when the value of "type" is a JSON Object, then the leaf is a YANG enumeration, and the CBOR Map gives the mapping between integer and strings.¶
when the value of "type" is a JSON Array, then the YANG leaf is a union. Elements of the Array list the possible types. In that case, the conversion is leaded by the CBOR Tags associated to the value.¶
We developed the pycoreconf Python module to facilitate the conversion between JSON and CBOR (https://github.com/ltn22/pycoreconf). The Figure 8 gives an example of a Python script using this module. It takes as input the .sid file and a JSON structure. It transforms it into a CBOR structure, and back to JSON representation.¶
The resulting JSON structure of Figure 3 is given in Figure 9. It shows that the JSON format can be converted to CBOR and vice versa, just by using the extended .sid file.¶
In some YANG Data Models, as the one provided in Figure 1, some leaf nodes might encompass information that extends beyond the model's boundaries. For example, the "statusLED" leaf may require an action to be triggered when a value is written onto it, or the managed device might need to interact with a physical sensor when the "sensorValue" leaf is queried. On the other hand, certain leaf nodes, such as "index", do not require any interaction as their values are directly stored in the database.¶
We wrote a script taking the .sid file obtained from pyang with the "--sid-extension" argument to produce template C code for these functions. We try keep SID transparent and use the YANG identifiers as much as possible, which are easier to manipulate by a programmer.¶
The tool aims to generate aliases for identifiers that also include details about their associated SIDs. This eliminates the need for developers to recall or verify SIDs during software development.¶
The tool is hosted at https://github.com/manojgudi/ccoreconf/tree/stub_generation/tools¶
For the SID file above (sensor@unknown.sid), the second argument in the command- "sensor_prototypes" instructs the tool to create two files sensor_prototypes.h and sensor_prototypes.c for writing headers and function code, respectively. To generate the C code, simply run the tool:¶
$ python generateStubs.py sensor@unknown.sid "sensor_prototypes"¶
//Headers //----------- #include <stdlib.h> #include <stdint.h> #include <stdbool.h> #include <string.h> #include <cbor.h> #define SID_BATTERY 60006 #define SID_INDEX 60008 #define SID_SENSORVALUE 60009 #define SID_STATUSLED 60010 #define read_sensorObject read_60005 #define read_battery read_60006 #define read_sensorReadings read_60007 #define read_sensorValue read_60009 #define read_statusLED read_60010 char* keyMapping = "\xa1\x19\xeag\x81\x19\xeah"; enum StatusledEnum {green = 0, yellow = 1, red = 2}; void read_sensorObject(void); char * read_battery(void); void read_sensorReadings(uint8_t index); uint32_t read_sensorValue(uint8_t index); enum StatusledEnum read_statusLED(void);¶
//Code File //----------- #include <stdlib.h> #include <stdint.h> #include <stdbool.h> #include <string.h> #include "sensor_prototypes.h" /* This is an autogenerated function associated to SID: 60005 Module: data Identifier: /sensor:sensorObject function params: Stable: false */ void read_sensorObject(void){ // Initialize the leaf if it has a return type with a default value; // Do something with the leaf } /* This is an autogenerated function associated to SID: 60006 Module: data Identifier: /sensor:sensorObject/battery function params: Stable: false */ char * read_battery(void){ // Initialize the leaf if it has a return type with a default value; char * batteryInstance = NULL; // Do something with the leaf // Return the leaf return batteryInstance; } /* This is an autogenerated function associated to SID: 60007 Module: data Identifier: /sensor:sensorObject/sensorReadings function params: /sensor:sensorObject/sensorReadings/index Stable: false */ void read_sensorReadings(uint8_t index){ // Initialize the leaf if it has a return type with a default value; // Do something with the leaf } /* This is an autogenerated function associated to SID: 60009 Module: data Identifier: /sensor:sensorObject/sensorReadings/sensorValue function params: /sensor:sensorObject/sensorReadings/index Stable: false */ uint32_t read_sensorValue(uint8_t index){ // Initialize the leaf if it has a return type with a default value; uint32_t sensorValueInstance = 0; // Do something with the leaf // Return the leaf return sensorValueInstance; } /* This is an autogenerated function associated to SID: 60010 Module: data Identifier: /sensor:sensorObject/statusLED function params: Stable: false */ enum StatusledEnum read_statusLED(void){ // Initialize the leaf if it has a return type with a default value; enum StatusledEnum statusLEDInstance; // Do something with the leaf // Return the leaf return statusLEDInstance; }¶
Let us take a quick look at the generated code snippet to understand how they help abstract SIDs from the developers:¶
The first part of the sensor_prototypes.h contains pre-processor directives which basically store the identifiers with their corresponding SID values for quick access. The second part contains function prototypes which can be implemented later. Note that there will not be any function prototype generated to read SID keys (in this case index, with SID 60008).¶
// Aliases of the leaves mapped to their corresponding SID #define SID_BATTERY 60006 #define SID_INDEX 60008 #define SID_SENSORVALUE 60009 #define SID_STATUSLED 60010 // Aliases of function prototypes linked to their function name containing SID value for quick and easy access #define read_sensorObject read_60005 #define read_battery read_60006 #define read_sensorReadings read_60007 #define read_sensorValue read_60009 #define read_statusLED read_60010 // The relation between certain identifiers and their sid keys is stored in keyMapping as a CBOR map char* keyMapping = "\xa1\x19\xeag\x81\x19\xeah"; enum StatusledEnum {green = 0, yellow = 1, red = 2}; // Function prototypes of the getters which can be implemented by the developer later in its corresponding C file // The return types of the functions are mapped according to the type of the leaf. The getters for non-leaves are mapped as void void read_sensorObject(void); char * read_battery(void); void read_sensorReadings(uint8_t index); uint32_t read_sensorValue(uint8_t index); enum StatusledEnum read_statusLED(void);¶
Now let us understand what is generated in sid_prototypes.c by looking at the auto-generated function prototype read_sensorValue. The program auto-infers that the leaf sensorValue can only be reached through a valid SID key (that is, index), so a function trying to read sensorValue from its CORECONF instance will require a parameter index. Also, since sensorValue has a well-defined type in C, the function will try to return that uint32_t.¶
The program also maps the enum type from SID to a corresponding enum type in C, as shown for the leaf statusLED. The value of these enums is as specified in the corresponding YANG model. If unspecified, the values are auto-assigned.¶
For rest of the non-leaf functions and leaf nodes with type identityref, the program infers it to be a char * (since the types are non-standard).¶
uint32_t read_sensorValue(uint8_t index){ // Initialize the leaf if it has a return type with a default value; uint32_t sensorValueInstance = 0; // Do something with the leaf // Return the leaf return sensorValueInstance; }¶