The following tutorial will breakdown step by step how to create Opendaylight project. During the tutorial we will briefly mentioned the messenger project modules that generated by ODL project archetype. Then we will we illustrate how yang model our Messenger example and generate its abstraction layer at Opendaylight. We will explain the difference between the Opendaylight's datasores (operational/config) and we will track the datastore changes to implement our messenger tutorial business logic. Finally we will create and implement RPC and Notification services to communicate with the Messenger through the REST APIs and Karaf CLI.
We assume that you already have java programming skills and you have knowledge of maven and apache karaf. If you have leak of knowledge about maven and apache karaf you still can go through the tutorial but we strongly recommend increase your knowledge in both subjects. You already have been setup you Opendaylight development environment using dev guid at https://wiki.opendaylight.org/view/GettingStarted:Development_Environment_Setup
The first we want to do is prepare the bundle structure, as such we will be using an archetype generator to create the project skeleton.
create your project with the archetype by typing:
mvn archetype:generate -DarchetypeGroupId=org.opendaylight.archetypes -DarchetypeArtifactId=opendaylight-startup-archetype \ -DarchetypeCatalog=remote -DarchetypeVersion=1.0.0-SNAPSHOT
Respond to the prompts (Please note that groupid and artifactid need to be all lower case):
Define value for property 'groupId': : org.opendaylight.messenger Define value for property 'artifactId': : messenger Define value for property 'version': 1.0-SNAPSHOT: : 0.1.0-SNAPSHOT Define value for property 'package': org.opendaylight.messenger: : Define value for property 'classPrefix': ${artifactId.substring(0,1).toUpperCase()}${artifactId.substring(1)} Define value for property 'copyright': : you/me/whatever
Once completed, the project structure should look like this:
[Root directory] api/ artifacts/ cli/ features/ impl/ it/ karaf/ pom.xml
That maven command has generated 7 bundles and 1 bundle aggregator. The aggregator is represented by the pom.xml file at the root of the project and it will "aggregate" the sub-bundles into "modules". This aggregator pom.xml file is of type "pom" so there are no jar files generated from this. The subfolders represent bundles and will also have their own pom.xml files, each of these file will generate a jar file at the end. It also creates the target/, src/ directories and deploy-site.xml. Both src/ and deploy-site.xml used for generating java doc.
Lets go over what each bundles (module) do:
- api : This is where we define the Messenger model. It has api in its name because it will be used by RestConf to define a set of rest APIs.
- artifacts: This is where the bundles gets generated as
- cli: This bundle is used to provides a Karaf CLI functionality to Messenger.
- features: This bundle is used to deploy the Messenger into the karaf instance. It contains a feature descriptor or features.xml file.
- impl: This is where we tell what to do with the Messenger. This bundle depends on the api to defines its operations.
- it: This is used to test the Messenger work within the integration test of Opendaylight.
- karaf: This is the instance in which we will be deploying our Messenger. Once compile, it creates a distribution that we can execute to run the karaf instance.
Here is highlighted the important sections of the aggregator pom.xml file: The ''pom.xml'' file works as aggregator and defines the parent project, and will declare the modules presented in the structure above:
... <groupId>org.opendaylight.odlparent</groupId> <artifactId>odlparent</artifactId> <version>1.7.1-Boron-SR1</version> <relativePath/> </parent> ... <groupId>org.opendaylight.messenger</groupId> <artifactId>messenger-aggregator</artifactId> <version>0.1.0-SNAPSHOT</version> <name>messenger</name> <packaging>pom</packaging> ... <modules> <module>api</module> <module>impl</module> <module>karaf</module> <module>features</module> <module>artifacts</module> <module>cli</module> <module>it</module> </modules> ...
The aggregator pom.xml file also include two build plugin to initiate the maven build process for the messenger project.
... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-install-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build> ...
Now we will use yang to model our messenger. The main functionality of any messenger is to deliver messages between to persons/objects source and destination. In order to model the messenger we need to model the message as it is part of the messenger main functionality. Under the api module src/main/yang we have defined two yang models messenger.yang and messenger-rpc.yang let's go through them. At messenger.yang we model the message as grouping having the basic message attributes such as mess-id, message-source, message-dest and message-text. I didn't set the message datetime attribute to let you add it while you re-doing the tutorial by yourself.
grouping message { leaf mess-id { description "message Id"; type string; mandatory true; } leaf text { description "message text"; type string; } leaf message-source { description "message source"; type string; } leaf message-dest { description "message dest"; type string; } }
Then we model the messenger as container contain the messenger basic messenger attributes and list of messages.
container messenger { leaf id { type string; mandatory true; } leaf connected { description "indicate if the messenger service is connected"; type boolean; default true; config false; } leaf last-message-datetime { type string; } list message { key mess-id; uses message; } }
The messenger "connected" attribute has "config false;" defintion which means the messenger connected attribute will be only available at the Opendaylight MD-SAL operational datastore. MD-SAL, along with some ietf drafts for restconf split the configuration and operational data into two separate data stores.
- Operational - Operational data stores are used to show the running state (read only) view of the devices, network, services, etc that you might be looking at. In our case we specify the messenger connection status.
- Config - Config data stores are generally used to configure the device in someway.These configurations are user provided and is a way for the user to tell the device how to behave. For example if you wanted to configure the resource in some way, such as applying a policy or other configuration then you would use this data store.
At the messenger-rpc.yang we defined two basic RPCs to communicate with the messenger send-message and get-last-message-datetime.
rpc send-message { description "Send a message"; input { uses messenger:message; } output { leaf message-id { type string; } } } rpc get-last-message-datetime { description "Get datetime of last message"; output { leaf message-datetime { type string; } } }
You can define more RPCs to support the messenger basic functionality such as get messages or get messenger connected. See the below example:
rpc is-messenger-connected { description "Get the messenger connection status"; output { leaf connected { type boolean; } } }
We really recommend include this RPC and implement its logic while we go through the implementation section.
Back to the messenger.yang we defined the messenger-connection notification which will let us change the messenger connection status between connected and disconnected.
notification messenger-connection { description "Notify the messenger to be connected or disconnected."; leaf connected { type boolean; } }
Now it is time to generating the abstract layer of our messenger model. under the messenger directory run the following command
mvn clean install
Now we will take a deeper look at the api module POM.xml file. You can see the parent of the api module is the binding-parent artifact.
<parent> <groupId>org.opendaylight.mdsal</groupId> <artifactId>binding-parent</artifactId> <version>0.9.1-Boron-SR1</version> <relativePath/> </parent>
The binding-parent artifact has the yangtool plugin code generator that will read all the yang files that exist in the api module and generate the MD-SAL binding classes that mapped to the yang models and save them under the target directory. You can check the binding-partent artifact at the following link: https://github.com/opendaylight/mdsal/blob/master/binding/binding-parent/pom.xml#L110
You should see java class files generated under api/generated-sources/mdsal-binding/. Classes of note:
-
Message, Messenger: interfaces that represents the messenger container and message grouping with methods to obtain the leaf node data.
-
MessengerData: an interface that represents the top-level messenger module with one method getMessenger() that returns the singleton messenger instance.
-
MessageKey: a class to represent the key leaf of the message list.
-
MessengerListener: an interface extend the NotificationListener that will be used to register the messenger notification to Opendaylight global notification service.
-
MessengerConnection: an interface that present messenger notification
-
MessengerRpcService: an interface extend the Opendaylight RpcService.
-
SendMessageInput, SendMessageOutput: interfaces represent the send-message rpc.
-
GetLastMessageDatetimeOutput: an interface represents get-last-message-datetime rpc.
-
$YangModelBindingProvider, $YangModuleInfoImpl - these are used internally by MD-SAL to wire the messenger module for use. More on this later.
for more details info about the yangtool, yang-to-java mapping and md-sal binding check the following links:
https://wiki.opendaylight.org/view/YANG_Tools:YANG_to_Java_Mapping https://wiki.opendaylight.org/view/OpenDaylight_Controller:MD-SAL:MD-SAL_Document_Review:MD_SAL#HOW_DOES_IT_WORK.3F https://wiki.opendaylight.org/view/YANG_Tools:Available_Models
Initially when you build the messenger project the impl module will have the MessengerProvider class and impl-blueprint.xml file that define the MessengerProvider bean. The MessengerProvider class require the DataBroker interface as contracture argument. Via Databroker we can create, update and delete the messenger datatree in both Md-SAL datastores operational and config.
public MessengerProvider(final DataBroker dataBroker) { this.dataBroker = dataBroker; }
<reference id="dataBroker" interface="org.opendaylight.controller.md.sal.binding.api.DataBroker" odl:type="default" /> <bean id="provider" class="org.opendaylight.messenger.impl.MessengerProvider" init-method="init" destroy-method="close"> <argument ref="dataBroker" /> </bean>
In our final implementation for the messenger project we have add three classes to help us apply the messenger business logic and functionalities.
MessengerMdsalUtils contains the CRUD methods that will help us read from and add to the messenger data tree at Md-SAL datastore
The DataBroker provides listeners to raise event when a specific datatree has been changed. In the MessageDataTreeChangeListener we listen to the created messages at the config datastore and add them to the operational datastore with updating the messenger last message datetime data.
MessengerService implement the MessengerRpcService interfcae and has the business logic of the messenger RPCs.
We have modified the MessengerProvider and implement the MessengerListener interface to let the MessengerProvider react when a notification sent to the messenger to change its connection status. Finally at the impl-blueprint.xml we refer to ODL NotificationService to be able to register the messenger notification listener and we defined our implementation of the messenger RPCs at the rpc-implementation tag.
<reference id="notificationService" interface="org.opendaylight.controller.md.sal.binding.api.NotificationService"/> <bean id="messengerRPC" class="org.opendaylight.messenger.impl.MessengerService"> <argument ref="provider" /> </bean> <odl:rpc-implementation ref="messengerRPC"/>
public class MessengerProvider implements MessengerListener { private static final Logger LOG = LoggerFactory.getLogger(MessengerProvider.class); private static final String MESSENGER_DATA_TREE = "Messenger:1"; private final MessageDataTreeChangeListener datatree; private final DataBroker dataBroker; public MessengerProvider(final DataBroker dataBroker, final NotificationService notificationSrv) { this.dataBroker = dataBroker; notificationSrv.registerNotificationListener(this); datatree = new MessageDataTreeChangeListener(dataBroker); } public void init() { LOG.info("MessengerProvider Session Initiated"); initializeMessengerDataTree(); } public void close() { LOG.info("MessengerProvider Closed"); try { datatree.close(); } catch (Exception e) { LOG.error("data tree close ", e); } } private void initializeMessengerDataTree() { if (MessengerMdsalUtils.read(dataBroker, LogicalDatastoreType.CONFIGURATION, MessengerMdsalUtils.getMessengerIid()) == null) { final Messenger messengerData = new MessengerBuilder().setId(MESSENGER_DATA_TREE).build(); MessengerMdsalUtils.initalizeDatastore(LogicalDatastoreType.CONFIGURATION, dataBroker, MessengerMdsalUtils.getMessengerIid(), messengerData); MessengerMdsalUtils.initalizeDatastore(LogicalDatastoreType.OPERATIONAL, dataBroker, MessengerMdsalUtils.getMessengerIid(), messengerData); } } ... @Override public void onMessengerConnection(MessengerConnection notification) { LOG.info("Notification to change the messenger connection."); final Messenger messenger = MessengerMdsalUtils.read(dataBroker, LogicalDatastoreType.OPERATIONAL, MessengerMdsalUtils.getMessengerIid()); final MessengerBuilder messengerBld = new MessengerBuilder(messenger).setConnected(notification.isConnected()); MessengerMdsalUtils.merge(dataBroker, LogicalDatastoreType.OPERATIONAL, MessengerMdsalUtils.getMessengerIid(), messengerBld.build()); } }
Initially the CLI module has the test-command that takes -tA test Argument. We modified the test-command to notify the messenger changes its status connected/disconnected. At the cli-blueprint.xml we refer to the NotificationPublishService to be able to send a notification to the messenger based on the desired status we want.
<reference id="notificationService" interface="org.opendaylight.controller.md.sal.binding.api.NotificationPublishService"/>
public class MessengerCliCommandsImpl implements MessengerCliCommands { private static final Logger LOG = LoggerFactory.getLogger(MessengerCliCommandsImpl.class); private final DataBroker dataBroker; private final NotificationPublishService notificationSrv; public MessengerCliCommandsImpl(final DataBroker db, final NotificationPublishService notificationSrv) { this.dataBroker = db; this.notificationSrv = notificationSrv; LOG.info("MessengerCliCommandImpl initialized"); } @Override public Object testCommand(Object testArgument) { MessengerConnection messConn; if (testArgument.equals("connect")) { messConn = new MessengerConnectionBuilder().setConnected(true).build(); notificationSrv.offerNotification(messConn); return "Messenger connected"; } else if (testArgument.equals("disconnect")) { messConn = new MessengerConnectionBuilder().setConnected(false).build(); notificationSrv.offerNotification(messConn); return "Messenger disconnected"; } return "Not vaild status"; } }
Now we will test what we have done so far, after your the build command.
mvn clean install -DskipTests
under the karaf module the messenger distribution should be created under the target directory run the following command to start the distribution.
./karaf/target/assembly/bin/karaf
Initially the messenger feature will be installed except the odl-messenger-cli feature. Run the following command at karaf CLI to install the messenger cli.
feature:install odl-messenger-cli
Now open a new terminal to communicate with the messenger via the REST APIs. We will retrive the data tree of the messenger from both config and operational datastores. use the following commands
- for config datastore:
curl -X GET -H "Authorization: Basic YWRtaW46YWRtaW4=" -H "Cache-Control: no-cache" "http://localhost:8181/restconf/config/messenger:messenger"
output should be:
{ "messenger": { "id": "Messenger:1" } }
- for operational datastore:
curl -X GET -H "Authorization: Basic YWRtaW46YWRtaW4=" -H "Cache-Control: no-cache" "http://localhost:8181/restconf/operational/messenger:messenger"
output should be:
{ "messenger": { "id": "Messenger:1" } }
We will use the send-message RPC to send a message using our messenger.
curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Basic YWRtaW46YWRtaW4=" -H "Cache-Control: no-cache" -d '{ "input": { "mess-id": "11", "text": "hello", "message-source": "192.168.1.11", "message-dest": "192.168.1.15" } }' "http://localhost:8181/restconf/operations/messenger-rpc:send-message"
output should be:
{ "output": { "message-id": "11" } }
Now use the previous two command to retrive the messenger datatree from config and operational datastores
- for config datastore output should be:
{ "messenger": { "id": "Messenger:1", "message": [ { "mess-id": "11", "text": "hello", "message-dest": "192.168.1.15", "message-source": "192.168.1.11" } ] } }
- for operational datastore output should be:
{ "messenger": { "id": "Messenger:1", "connected": true, "last-message-datetime": "Mon Nov 28 15:28:39 EST 2016", "message": [ { "mess-id": "11", "text": "hello", "message-dest": "192.168.1.15", "message-source": "192.168.1.11" } ] } }
As you can see the operational datastore has the connected attribute and the config datastore data does not have it.
Now we will back to the messenger distribution karaf CLI and notify the messenger to change its connected status to false using the following command.
messenger:test-command -tA disconnect
Try to retrive the messenger operational datastore data it should be like that:
{ "messenger": { "id": "Messenger:1", "connected": false, "last-message-datetime": "Mon Nov 28 15:28:39 EST 2016", "message": [ { "mess-id": "11", "text": "hello", "message-dest": "192.168.1.15", "message-source": "192.168.1.11" } ] } }
Now if you tried to send a new message using the send-message RPC it should fail as the messenger is disconnected.