YANG is a data modeling language, which allows modeling of Remote Procedure Calls (RPCs).
YANG is widely used in networking for modeling configurational and operational data of network devices (more than just routers and switches). These devices also implement some of the well-defined protocols for transport of the data modeled in YANG e.g.: NETCONF, RESTCONF, or GRPC.
Network controller software can manage such devices, by implementing these protocols and using the YANG models of the configuration and operational data.
This way, the controller or other software applications on top of the controller can configure and monitor a network of huge size. OpenDaylight is the leading open source SDN controller, which provides such functionalities.
Meet lighty.io, which is based on OpenDaylight components with additional PANTHEON.tech plugins & tools for fast development of lightweight SDN applications. One of the provided extensions by lighty.io is a unified API for YANG modeled data transformations.
This API is important for the integration of lighty.io with external systems. This article describes how to model RPC in YANG modeling language, how to generate Java classes from the YANG model, and how to implement the modeled RPC over HTTP protocol using the Codecs component of lighty.io.
The YANG model of the example RPC (see the yang model file for more details) is pretty straight forward:
rpc compute-sum { input { leaf operand-a { type uint16; mandatory true; } leaf operand-b { type uint16; mandatory true; } } output { leaf sum { type uint32; mandatory true; } } }
The input of the RPC consists of two numbers and the output represents the sum of input numbers.
OpenDaylight – YANG – Maven
The ODL yang-maven-plugin is used to generate Java classes and interfaces from the Yang model (see the pom.xml of this example for more details).
An interface called LightyRpcExampleService is generated, which defines the RPC as a Java method with the input container as argument and output container as a return value (wrapped by Future<RpcResult<>>).
Now we need to provide an implementation for this freshly generated interface:
package tech.pantheon.lighty.examples.rpc; import org.opendaylight.yang.gen.v1.urn.pantheon.params.xml.ns.yang.lighty.rpc.example.rev180219.ComputeSumInput; import org.opendaylight.yang.gen.v1.urn.pantheon.params.xml.ns.yang.lighty.rpc.example.rev180219.ComputeSumOutput; import org.opendaylight.yang.gen.v1.urn.pantheon.params.xml.ns.yang.lighty.rpc.example.rev180219.ComputeSumOutputBuilder; import org.opendaylight.yang.gen.v1.urn.pantheon.params.xml.ns.yang.lighty.rpc.example.rev180219.LightyRpcExampleService; import org.opendaylight.yangtools.yang.common.RpcResult; import org.opendaylight.yangtools.yang.common.RpcResultBuilder; import java.util.concurrent.Future; public class LightyRpcExampleServiceImpl implements LightyRpcExampleService { public Future<RpcResult<ComputeSumOutput>> computeSum(ComputeSumInput input) { return RpcResultBuilder.success( new ComputeSumOutputBuilder() .setSum((long) input.getOperandA() + input.getOperandB()) .build()) .buildFuture(); } }
The next step is to write a code, which will decode serialized input (received as HTTP payload), call the RPC, and serialize the resulting output to be sent back to the client.
In the beginning, it is needed to initialize a schema context including the YANG model which is needed for serialization and deserialization of modeled data.
This example shows how the schema context and RPC definition objects are initialized in the constructor of class RpcHttpServer from this example:
System.out.println("Loading Yang model schema context"); /* Load YANG models available at the class path */ List<YangModuleInfo> moduleInfos = new LinkedList<>(); ServiceLoader<YangModelBindingProvider> yangProviderLoader = ServiceLoader.load(YangModelBindingProvider.class); for (YangModelBindingProvider yangModelBindingProvider : yangProviderLoader) { moduleInfos.add(yangModelBindingProvider.getModuleInfo()); } /* Create the schema context for loaded models */ ModuleInfoBackedContext moduleInfoBackedCntxt = ModuleInfoBackedContext.create(); moduleInfoBackedCntxt.addModuleInfos(moduleInfos); Optional<SchemaContext> schemaCtx = moduleInfoBackedCntxt.tryToCreateSchemaContext().toJavaUtil(); if (!schemaCtx.isPresent()) { throw new IllegalStateException("Failed to load schema context"); } this.schemaContext = schemaCtx.get(); System.out.println("Yang model schema context loaded"); /* Load the RPC used in this example */ Optional<RpcDefinition> loadRpc = ConverterUtils.loadRpc(schemaContext, rpcServiceQname); if (!loadRpc.isPresent()) { throw new IllegalStateException("RPC definition not found"); } this.rpcDef = loadRpc.get(); System.out.println("Loaded definition of RPC: " + this.rpcDef.getQName().getLocalName());
The last code snippet shows how to deserialize the incoming payload of HTTP request encoded in JSON using a coded object. The method implementing RPC is called with the input object as an argument, while the output object is obtained as a return value of the method.
The output object is then serialized to JSON using the same codec object as for input HTTP payload. The output JSON data can be written into HTTP response payload (see the example source code for more details).
/* Read the payload from HTTP request */ final String payload = com.google.common.io.CharStreams.toString(req.getReader()); if (payload.isEmpty()) { System.out.println("Nothing received"); return; } else { System.out.println("Received payload: " + payload); } /* Create codec for the RPC input and load the RPC definition */ DataCodec<ComputeSumInput> dataCodec = new DataCodec<>(schemaContext); /* Deserialize the payload to normalized node */ NormalizedNode rpcInputNode = dataCodec.withJson().deserialize(rpcDef, new StringReader(payload)); /* Convert the normalized node to RPC's input object */ ComputeSumInput rpcInput = dataCodec.convertToBindingAwareRpc(rpcDef.getInput().getPath(), (ContainerNode) rpcInputNode); /* Call the RPC and get RPC output */ ComputeSumOutput rpcOutput = rpcImpl.computeSum(rpcInput).get().getResult(); /* Serialize the RPC output */ Writer output = dataCodec.withJson().serializeRpc(rpcDef.getOutput(), dataCodec.convertToBindingIndependentRpc(rpcOutput)); System.out.println("RPC output: " + output.toString());
You can use the following to test the RPC, and see the output:
curl -H “Content-Type: application/json” -X POST -d ‘{ “input”: { “operand-a”: 200, “operand-b”: 22 }}’ http://localhost:8080
{“output”:{“sum”:222}}
A sample output from the Java app above:
Loading Yang model schema context Yang model schema context loaded Loaded definition of RPC: compute-sum HTTP server started at port 8080 Press any key to exit... Received payload: { "input": { "operand-a": 200, "operand-b": 22 }} RPC output: {"output":{"sum":222}}