Developing OSGi Components for OpenDaylight

In this tutorial, I will explain how to develop an OSGi component for OpenDaylight that is implementing custom network control logic. In contrast to the REST interface, which I have explained in one of my last posts, OSGi components can receive packet-in events, which are triggered when a packet without matching flow table entry arrives at a switch. Therefore, in order to do reactive flow programming, OSGi components are the right way to go in OpenDaylight.

Even for experienced Java programmers, the learning curve for developing OSGi components for OpenDaylight is quite steep. OpenDaylight uses powerful development tools and techniques like Maven and OSGi. Moreover, the project structure is quite complex and the number of Java classes overwhelming at first. However, as you will see in this tutorial, the development process is quite straightforward and thanks to Maven very convenient.

In order to explain everything step by step, I will go through the development of a simple OSGi component. This component does nothing special. It basically displays a message when an IPv4 packet is received to show the destination address, data path id, and ingress port. However, you will learn many things that will help you in developing your own control components like:

  • How to setup an OpenDaylight Maven project?
  • How to install, uninstall, start, and stop an OSGi bundle in OpenDaylight at runtime?
  • How to manage the OSGi component dependencies and life-cycle?
  • How to receive packet-in events through data packet listeners?
  • How to decode packets using the OpenDaylight Data Packet Service

I should note here, that I will use the so-called API-driven Service Abstraction Layer (SAL) of OpenDaylight. OpenDaylight implements a second alternative API called the Model-driven SAL. This API I might cover in a future post.

So let’s get started!

The Big Picture

The figure below shows the architecture of our system. It consists of a number of OSGi bundles that bundle together Java classes, resources, and a manifest file. One of these bundles called the MyControlApp bundle is the bundle we are developing in this tutorial. Other bundles are coming from the OpenDaylight project like the SAL (Service Abstraction Layer) bundle.

Bundles are executed atop the OSGi Framework (Equinox in OpenDaylight). The interesting thing about OSGi is that bundles can be installed and removed at runtime, so you do not have to stop the SDN controller to add or modify control logic.

opendaylight-osgi

As you can also see, OSGi bundles are offering services that can be called by other OSGi components. One interesting service that comes with OpenDaylight and that we will use during this tutorial is the Data Packet Service (interface IDataPacketService) to decode data packets.

Although our simple control component is not offering functionality to any other bundle, it is important to understand that in order to receive packet-in events, it has to offer a service implementing the IListenDataPacket interface. Whenever an OpenFlow packet-in event arrives at the controller, the SAL invokes the components that implement the IListenDataPacket interface, among them our bundle.

Prerequisites

Before we start developing our component, we should get a running copy of OpenDaylight. Lately, the first release version of OpenDaylight was released. You can get a copy from this URL.

Or you can get the latest version from the OpenDaylight GIT repository and compile it yourself:

user@host:$ git clone https://git.opendaylight.org/gerrit/p/controller.git
user@host:$ cd ./controller/opendaylight/distribution/opendaylight/
user@host:$ mvn clean install

Actually, in order to develop an OpenDaylight OSGi component, you do not need the OpenDaylight source code! As we will see below, we can just import the required components as JARs from the OpenDaylight repository.

During the compile process, you see that Maven downloads many Java packages on the fly. If you have never used Maven before, this can be quite confusing. Haven’t we just downloaded the complete project with git? Actually, Maven can automatically download project dependencies (libraries, plugins) from a remote repository and place them into your local repository so they are available during the build process. Your local repository usually resides in ~/.m2. If you look into this repository after you have compiled OpenDaylight, you will see all the libraries that Maven downloaded:

user@host:$ ls ~/.m2/repository/
antlr                     classworlds          commons-fileupload  dom4j          jline  regexp
aopalliance               com                  commons-httpclient  eclipselink    junit  stax
asm                       commons-beanutils    commons-io          equinoxSDK381  log4j  virgomirror
backport-util-concurrent  commons-cli          commons-lang        geminiweb      net    xerces
biz                       commons-codec        commons-logging     io             orbit  xml-apis
bsh                       commons-collections  commons-net         javax          org    xmlunit
ch                        commons-digester     commons-validator   jfree          oro

For instance, you see that Maven has downloaded the Apache Xerces XML parser. We will come back to this nice feature later when we discuss our project dependencies.

I will refer to the root directory of the controller as ~/controller in the following.

Creating the Maven Project

Now we start developing our OSGi component. Since OpenDaylight is based on Maven, it is a good idea to also use Maven for our own project. So we start by creating a Maven project for our OSGi component. First, create the following project structure. I will refer to the root directory of our component as ~/myctrlapp:

myctrlapp
  |--src
       |--main
           |--java
               |--de
                   |--frank_durr
                       |--myctrlapp

Obviously, Java implementations go into the folder src/main/java. I used the package de.frank_durr.myctrlapp for the implementation of my control component.

Essential to the Maven build process is a so-called Project Object Model (POM) file called pom.xml that you have to create in the folder ~/myctrlapp with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>de.frank_durr</groupId>
    <artifactId>myctrlapp</artifactId>
    <version>0.1</version>
    <packaging>bundle</packaging>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.3.7</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Import-Package>
                            *
                        </Import-Package>
                        <Export-Package>
                            de.frank_durr.myctrlapp
                        </Export-Package>
                        <Bundle-Activator>
                            de.frank_durr.myctrlapp.Activator
                        </Bundle-Activator>
                    </instructions>
                    <manifestLocation>${project.basedir}/META-INF</manifestLocation>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.opendaylight.controller</groupId>
            <artifactId>sal</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>

    <repositories>
        <!-- OpenDaylight releases -->
        <repository>
            <id>opendaylight-mirror</id>
            <name>opendaylight-mirror</name>
            <url>http://nexus.opendaylight.org/content/groups/public/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>never</updatePolicy>
            </releases>
        </repository>
        <!-- OpenDaylight snapshots -->
        <repository>
            <id>opendaylight-snapshot</id>
            <name>opendaylight-snapshot</name>
            <url>http://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
</project>

First, we define our group id (unique id of our organization) and artifact id (name of our component/project) as well as a version number. The packaging element specifies that an OSGi bundle (JAR file with classes, resources, and manifest file) should be built.

During the Maven build process, plugins are invoked. One very important plugin here is the Bundle plugin from the Apache Felix project that creates our OSGi bundle. The import element specifies every package that should be imported by the bundle. The wildcard * imports “everything referred to by the bundle content, but not contained in the bundle” [Apache Felix], which is reasonable and much less cumbersome than specifying the imports explicitly. Moreover, we export every implementation from our package.

The bundle activator is called during the life-cycle of our bundle when it is started or stopped. Below I show how it is used to register for services used by our component and how to export the interface of our component.

The dependency element specifies other packages to which our component has a dependency. Remember when I said that Maven will download required libraries (JARs) automatically to your local repository in ~/.m2? Of course, it can only do that if you tell Maven what you need. We basically need the API-driven Service Abstraction Layer (SAL) of OpenDaylight. The OpenDaylight project provides an own repository with the readily-compiled components (see repositories element). Thus, Maven will download the JARs from this remote repository. No need to import all the source code of OpenDaylight into Eclipse! In my example, I use the release version 0.7.0. You can also use a snapshot by changing the version to 0.7.0-SNAPSHOT (or whatever version is available in the snapshot repository; just browse the repository URL given above to find out). If you need further packages, have a look at the central Maven repository.

From this POM file, you can now create an Eclipse project by executing:

user@host:$ cd ~/myctrlapp
user@host:$ mvn eclipse:eclipse

Remember to re-create the Eclipse project using this command, when you make changes to the POM.

Afterwards, you can import the project into Eclipse:

  • Menu Import / General / Existing projects into workspace
  • Select root folder ~/myctrlapp

Implementation of OSGi Component: The Activator

In order to implement our OSGi component, we only need two class files: an OSGi activator registering our component with the OSGi framework and a packet handler implementing the control logic and executing actions whenever a packet-in event is received.

First, we implement the activator by creating the file Activator.java in the directory ~/myctrlapp/src/main/java/frank_durr/myctrlapp:

package de.frank_durr.myctrlapp;
import java.util.Dictionary;
import java.util.Hashtable;

import org.apache.felix.dm.Component;
import org.opendaylight.controller.sal.core.ComponentActivatorAbstractBase;
import org.opendaylight.controller.sal.packet.IDataPacketService;
import org.opendaylight.controller.sal.packet.IListenDataPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Activator extends ComponentActivatorAbstractBase {

    private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);

    public Object[] getImplementations() {
        log.trace("Getting Implementations");

        Object[] res = { PacketHandler.class };
        return res;
    }

    public void configureInstance(Component c, Object imp, String containerName) {
        log.trace("Configuring instance");

        if (imp.equals(PacketHandler.class)) {

            // Define exported and used services for PacketHandler component.

            Dictionary<String, Object> props = new Hashtable<String, Object>();
            props.put("salListenerName", "mypackethandler");

            // Export IListenDataPacket interface to receive packet-in events.
            c.setInterface(new String[] {IListenDataPacket.class.getName()}, props);

            // Need the DataPacketService for encoding, decoding, sending data packets
            c.add(createContainerServiceDependency(containerName).setService(IDataPacketService.class).setCallbacks("setDataPacketService", "unsetDataPacketService").setRequired(true));

        }
    }
}

We extend the base class ComponentActivatorAbstractBase from the OpenDaylight controller. Developers already familiar with OSGi know that there are two methods start() and stop() that are called by the OSGi framework when the bundle is started or stopped, respectively. These two methods are overridden in the class ComponentActivatorAbstractBase to mange the life-cycle of an OpenDaylight component. From there, the two methods getImplementations() and configureInstance() are called.

The method getImplementations() returns the classes implementing components of this bundle. A bundle can implement more than one component, for instance, a packet handler for ARP requests and one for IP packets. However, our bundle just implements one component: the one reacting to packet-in events, which is implemented by our PacketHandler class (the second class described below). So we just return one implementation.

Method configureInstance() configures the component and, in particular, declares exported service interfaces and the services used. Since an OSGi bundle can implement more than one component, it is good style to check, which component should be configured in line 26.

Then we declare the services exported by our component. Recall that in order to receive packet-in events, the component has to implement the service interface IListenDataPacket. Therefore, by specifying that our class PacketHandler implements this interface in line 34, we implicitly register our component as listener for packet-in events. Moreover, we have to give our listener a name (line 31) using the property salListenerName. If you want to understand in detail, what is happening during registration, I recommend to have a look at the method setListenDataPacket() of class org.opendaylight.controller.sal.implementation.internal.DataPacketService. There you will see that so far, packet handlers are called sequentially. There might be many components that have registered for packet-in events, and you cannot force OpenDaylight to call your listener first before another one gets the event. So the order in which listeners are called is basically unspecified. However, you can create dependency lists using the property “salListenerDependency”. Moreover, using the property “salListenerFilter” you can set a org.opendaylight.controller.sal.match.Match object for the listener to filter packets according to header fields. Otherwise, you will receive all packets (if not other listener consumes it before our handler is called; see below).

Besides exporting our packet listener implementation, we also use other services. These dependencies are declared in line 37. In our example, we only use one service implementing the IDataPacketService interface. You might say now, “fine, but how do I get the object implementing this service to call it?”. To this end, you define two callback functions as part of your component class (PacketHandler), here called setDataPacketService() and unsetDataPacketService(). These callback functions are called with a reference to the service (see implementation of PacketHandler below).

Implementation of OSGi Component: The Packet Handler

The second part of our implementation is the packet handler, which receives packet-in events (the class that you have configured through the activator above). To this end, we implement the class PacketHandler by creating the following file PacketHandler.java in the directory ~/myctrlapp/src/main/java/frank_durr/myctrlapp:

package de.frank_durr.myctrlapp;

import java.net.InetAddress;
import java.net.UnknownHostException;

import org.opendaylight.controller.sal.core.Node;
import org.opendaylight.controller.sal.core.NodeConnector;
import org.opendaylight.controller.sal.packet.Ethernet;
import org.opendaylight.controller.sal.packet.IDataPacketService;
import org.opendaylight.controller.sal.packet.IListenDataPacket;
import org.opendaylight.controller.sal.packet.IPv4;
import org.opendaylight.controller.sal.packet.Packet;
import org.opendaylight.controller.sal.packet.PacketResult;
import org.opendaylight.controller.sal.packet.RawPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PacketHandler implements IListenDataPacket {

    private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);
    private IDataPacketService dataPacketService;

    static private InetAddress intToInetAddress(int i) {
        byte b[] = new byte[] { (byte) ((i>>24)&0xff), (byte) ((i>>16)&0xff), (byte) ((i>>8)&0xff), (byte) (i&0xff) };
        InetAddress addr;
        try {
            addr = InetAddress.getByAddress(b);
        } catch (UnknownHostException e) {
            return null;
        }

        return addr;
    }

    /*
     * Sets a reference to the requested DataPacketService
     * See Activator.configureInstance(...):
     * c.add(createContainerServiceDependency(containerName).setService(
     * IDataPacketService.class).setCallbacks(
     * "setDataPacketService", "unsetDataPacketService")
     * .setRequired(true));
     */
    void setDataPacketService(IDataPacketService s) {
        log.trace("Set DataPacketService.");

        dataPacketService = s;
    }

    /*
     * Unsets DataPacketService
     * See Activator.configureInstance(...):
     * c.add(createContainerServiceDependency(containerName).setService(
     * IDataPacketService.class).setCallbacks(
     * "setDataPacketService", "unsetDataPacketService")
     * .setRequired(true));
     */
    void unsetDataPacketService(IDataPacketService s) {
        log.trace("Removed DataPacketService.");

        if (dataPacketService == s) {
            dataPacketService = null;
        }
    }

    @Override
    public PacketResult receiveDataPacket(RawPacket inPkt) {
        log.trace("Received data packet.");

        // The connector, the packet came from ("port")
        NodeConnector ingressConnector = inPkt.getIncomingNodeConnector();
        // The node that received the packet ("switch")
        Node node = ingressConnector.getNode();

        // Use DataPacketService to decode the packet.
        Packet l2pkt = dataPacketService.decodeDataPacket(inPkt);

        if (l2pkt instanceof Ethernet) {
            Object l3Pkt = l2pkt.getPayload();
            if (l3Pkt instanceof IPv4) {
                IPv4 ipv4Pkt = (IPv4) l3Pkt;
                int dstAddr = ipv4Pkt.getDestinationAddress();
                InetAddress addr = intToInetAddress(dstAddr);
                System.out.println("Pkt. to " + addr.toString() + " received by node " + node.getNodeIDString() + " on connector " + ingressConnector.getNodeConnectorIDString());
                return PacketResult.KEEP_PROCESSING;
            }
        }
        // We did not process the packet -> let someone else do the job.
        return PacketResult.IGNORED;
    }
}

As you can see, our handler implements the listener interface IListenDataPacket. This interface declares the function receiveDataPacket(), which is called with the raw packet after a packet-in event from OpenFlow.

In order to parse the raw packet, we use the OpenDaylight Data Packet Service (object dataPacketService). As described for the activator, during the component configuration, we set two callback functions in our packet handler implementation, namely, setDataPacketService() and unsetDataPacketService(). Method setDataPacketService() is called with a reference to the data packet service, which is then used for parsing raw packets. After receiving a raw packet “inPkt”, we call dataPacketService.decodeDataPacket(inPkt) to get a layer 2 frame. Using instanceof, we can check for the class of the returned packet. If it is an Ethernet frame, we go on and get the payload from this frame, which is the layer 3 packet. Again, we check the type, and if it is an IPv4 packet, we dump the destination address.

Moreover, the example shows how to determine the node (i.e., switch) that received the packet and connector (i.e., port) on which the packet was received (lines 72 and 75).

Finally, we decide whether the packet should be further processed by another handler, or whether we want to consume the packet by returning a corresponding return value. PacketResult.KEEP_PROCESSING says, our handler has processed the packet, but others should also be allowed to do so. PacketResult.CONSUME means, no other handler after us receives the packet anymore (as described above, handlers are sorted in a list and called sequentially). PacketResult.IGNORED says, packet processing should go on since we did not handle the packet.

Deploying the OSGI Bundle

Now that we have implemented our component, we can first compile and bundle it using Maven:

user@host:$ cd ~/myctrlapp
user@host:$ mvn package

If our POM file and code are correct, this should create the bundle (JAR file) ~/myctrlapp/target/myctrlapp-0.1.jar.

This bundle can now be installed in the OSGi framework Equinox of OpenDaylight. First, start the controller:

user@host:$ cd ~/controller/opendaylight/distribution/opendaylight/target/distribution.opendaylight-osgipackage/opendaylight/
user@host:$ ./runs.sh

In the OSGi console install the bundle by specifying its URL:

osgi> install file:/home/user/myctrlapp/target/myctrlapp-0.1.jar
Bundle id is 256

We see that the id of our bundle is 256. Using this id, we can start the bundle next:

osgi> start 256

You can check, whether it is running by listing all OSGi bundles using the command ss:

osgi> ss
...
251 ACTIVE org.opendaylight.controller.hosttracker.implementation_0.5.1.SNAPSHOT
252 ACTIVE org.opendaylight.controller.sal-remoterpc-connector_1.0.0.SNAPSHOT
253 ACTIVE org.opendaylight.controller.config-persister-api_0.2.3.SNAPSHOT
256 ACTIVE de.frank_durr.myctrlapp_0.1.0

Similarly, you can stop and uninstall the bundle using the commands stop and uninstall, respectively:

osgi> stop 256
osgi> uninstall 256

Before we test our bundle, we stop two OpenDaylight services, namely, the Simple Forwarding Service and Load Balancing Service:

osgi> ss | grep simple
171 ACTIVE org.opendaylight.controller.samples.simpleforwarding_0.4.1.SNAPSHOT
true
osgi> stop 171
osgi> osgi> ss | grep loadbalancer
150 ACTIVE org.opendaylight.controller.samples.loadbalancer.northbound_0.4.1.SNAPSHOT
187 ACTIVE org.opendaylight.controller.samples.loadbalancer_0.5.1.SNAPSHOT
true
osgi> stop 187

Why did we do that? Because these are the two services also implementing a packet listener. For testing, we do want to make sure, they are not getting in our way and consuming packets before we can get them.

Testing

For testing, we use a simple linear Mininet topology with two switches and two hosts connected at the ends of the line:

user@host:$ sudo mn --controller=remote,ip=129.69.210.89 --topo linear,2

The given IP is the IP of our controller host.

Now let’s ping host 2 from host 1 and see the output in the OSGi console:

mininet> h1 ping h2

osgi>
Pkt. to /10.0.0.2 received by node 00:00:00:00:00:00:00:01 on connector 1
Pkt. to /10.0.0.1 received by node 00:00:00:00:00:00:00:02 on connector 1

You see that our handler received a packet from both switches with the data path ids 00:00:00:00:00:00:00:01 and 00:00:00:00:00:00:00:02 as well as the ports (1) on which they have been received and the destination IP addresses 10.0.0.2 and 10.0.0.1. So it worked.

Where to go from here?

What I did not show in this tutorial is how to send a packet. If you join me again, you can see that in one of my next tutorials here on this blog.

6 thoughts on “Developing OSGi Components for OpenDaylight

    • You should create the described basic directory structure manually. Then, you can create the Eclipse project files automatically by executing the command:

      $> mvn eclipse:eclipse

      from the root directory (~/myctrlapp in the given example). Afterwards, you can import the project into Eclipse.

      –Frank

  1. Hello,

    I am trying to use this example but it seems that the receiveDataPacket method is never initiated. I have stopped other OpenDaylight services that also implement a packet listener but this is not helping either. I am using the controller together with Cisco c891 router.

    Thank you in advance,
    K

    • Hello,

      it’s hard to say, where things might go wrong in your setup. I’d suggest to

      - try it with Mininet first, just to see whether it’s a problem with the router
      - download the example from my next post and try it to see whether there might be a problem copying code from this post.
      - check the log output to make sure that all necessary methods for registering for packet-in events have been called (you can adjust the log level using “setLogLevel de.frank_durr.myctrlapp.PacketHandler trace”). It’s important that the mehthods configureInstance(…) and getImplementations(…) have been invoked.
      - use tcpdump or Wireshark on your controller host to see whether packet-in events actually arrived at the host (this is quite low-level).

      HTH

      –Frank

Comments are closed.