Creating a Network Tapping Application in ONOS

Setting up the ONOS Application Project

This tutorial assumes you have installed ONOS (version 1.2) and have set up included the path to the development tools in the .bash_profile. For more information on how to install ONOS visit the ONOS wiki or watch the video on how to setup ONOS locally.

We also used the Mininet VM to run the OpenFlow switches.

To begin we will need to generate the the base project files from the ONOS Maven archetypes. To allow your application to add commands to the ONOS CLI, overlay the CLI interface like this:

$ onos-create-app

groupId: com.villatech
artifactId: tapping-example
version: 1.0-SNAPSHOT
package: com.villatech.tapping-example

$ cd tapping-example
$ onos-create-app cli com.villatech.tapping-example tapping-example 1.0.0

By default a couple lines are commented out in the pom.xml file. To create the .oar files for ONOS to install we need to uncomment these lines. Open the pom.xml in the root directory of the project and change the properties node to the following:


<properties>
    <onos.version>1.2.3-SNAPSHOT</onos.version>
    <onos.app.name>com.villatech.tapping-example</onos.app.name>
    <onos.app.origin>VILLA-TECH, Inc.</onos.app.origin>
</properties>

Adding Morphia ORM (MongoDB) to the project

We used MongoDB within this application so that we are able to monitor the flows and analyse the use case of the tapping application. To add Morphia ORM to the project a couple dependancies need adding to the pom.xml file. Under the dependencies node we need to add:


<dependencies>

        ...

        <dependency>
            <groupId>org.mongodb.morphia</groupId>
            <artifactId>morphia</artifactId>
            <version>1.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>com.thoughtworks.proxytoys</groupId>
            <artifactId>proxytoys</artifactId>
            <version>1.0</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>

        ...

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.5.3</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>
                            ${project.groupId}.${project.artifactId}
                        </Bundle-SymbolicName>
                        <Include-Resource>
                            {maven-resources}
                        </Include-Resource>
                        <Import-Package>
                            org.slf4j,
                            org.onlab.rest.*,
                            org.onosproject.*,
                            org.onlab.util.*,
                            javax.ws.rs,
                            javax.ws.rs.core,
                            javax.ws.rs.ext,
                            com.google.common.base.Preconditions,
                            com.fasterxml.jackson.databind,
                            com.fasterxml.jackson.databind.node,
                            com.fasterxml.jackson.core,
                            com.sun.jersey.api.core,
                            com.sun.jersey.json,
                            com.sun.jersey.spi.container.servlet,
                            com.sun.jersey.server.impl.container.servlet,
                            org.apache.karaf.shell.commands,
                            com.mongodb.*,
                            org.mongodb.morphia,
                            org.mongodb.morphia.query.*,
                            org.bson.*
                        </Import-Package>
                        <Export-Package>
                            com.thoughtworks.*,
                            com.thoughtworks.proxy.kit,
                            com.fasterxml.jackson.core,
                            com.sun.jersey.json,
                            com.google.common.base.Preconditions
                        </Export-Package>
                    </instructions>
                </configuration>
            </plugin>

            ...

The Database Connection Class

For this example only one instance of the Database Connection will be created

public class DatabaseConnection {

    private static DatabaseConnection instance = null;
    protected DatabaseConnection() {}

    private static final Logger logger = LoggerFactory.getLogger(DatabaseConnection.class);

    private Datastore database;

    public Datastore getDatabaseConnection() {

        if (database == null) {
            logger.debug("MongoDB: Connecting to MongoDB");
            return connect();
        }

        logger.debug("MongoDB: Returning established connection");
        return database;
    }

    private Datastore connect () {


        final Morphia morphia = new Morphia();

        // tell Morphia where to find your classes
        // can be called multiple times with different packages or classes
        morphia.mapPackage("org.villatech.tapping.models");

        database = morphia.createDatastore(new MongoClient(), "vt_tapping");
        database.ensureIndexes();

        return database;
    }

    public static DatabaseConnection getInstance() {
        if(instance == null) {
            instance = new DatabaseConnection();
        }

        return instance;
    }
}

Creating the Morphia Models

Morphia uses annotations to map the models into MongoDB Documents. Read more about the Morphia Annotations. In this example we will keep it simple and just create two models. Device and Capture Device.

public class Device {
    
    @Id
    private ObjectId _id;

    private int switchPort;
    private long ipAddr;
    private String macAddr = "";
    private int vlanId;

    @Reference
    List<CaptureDevice> captureDevList = new ArrayList<CaptureDevice>();
}
public class CaptureDevice {

    private long referenceCount = 0;

    @Id private ObjectId _id;

    private String name = "";
    private String switchId = "";
    private int switchPort;
    private long ipAddr;
    private String macAddr = "";
    private int vlanId;
}

Extending the ONOS CLI

We have already added the CLI dependancies and sample file to our project. Now we need to add the commands we need for our applications. Again, to keep this example simple we will just be adding one command to create the flow to and from a host device and the capture device.

At the top of the AppCommand file you will find a @Command annotation with scope, name and description properties set. We will set the scope to “tap” and the name to “create”. This will allow us to run “tap:create” from the ONOS command line.

The @Argument annotations define the arguments used in the command.

@Command(scope = "tap", name = "create", description = "Create Host to Capture Device Flow")
public class AppCommand extends ConnectivityIntentCommand {

    @Argument(index = 0, name = "device", description = "Host Device ID", required = true, multiValued = false)
    String device = null;

    @Argument(index = 1, name = "captureDevice", description = "Capture Device ID", required = true, multiValued = false)
    String captureDevice = null;

    @Override
    protected void execute() {

        IntentService service = get(IntentService.class);

        HostId deviceId = HostId.hostId(device);
        HostId captureDeviceId = HostId.hostId(captureDevice);

        TrafficSelector selector = buildTrafficSelector();
        TrafficTreatment treatment = buildTrafficTreatment();
        List constraints = buildConstraints();

        HostToHostIntent intent = HostToHostIntent.builder()
                .appId(appId())
                .key(key())
                .one(deviceId)
                .two(captureDeviceId)
                .selector(selector)
                .treatment(treatment)
                .constraints(constraints)
                .priority(priority())
                .build();
        service.submit(intent);
        print("Device to Capture Device intent submitted:\n%s", intent.toString());

        TapController tapController = new TapController();
        tapController.createFlowEntries(deviceId, captureDeviceId);
    }
}

The Controller

For this example I’ve kept it simple. The controller, in this case, is just going to keep track of how many flows have directed assigned to the capture device.

public class TapController {


    public CaptureDevice createOrUpdateCaptureDevice(String capture) {

        DatabaseConnection dbConnection = DatabaseConnection.getInstance();
        Datastore datastore = dbConnection.getDatabaseConnection();

        CaptureDevice captureDevice = datastore.find(CaptureDevice.class, "switchId", capture.toString()).get();

        if (captureDevice != null) {

            captureDevice.setReferenceCount(captureDevice.getReferenceCount() + 1);
            return captureDevice;

        } else {

            CaptureDevice newCaptureDevice = new CaptureDevice();
            newCaptureDevice.setSwitchId(capture.toString());
            newCaptureDevice.setReferenceCount(1);

            datastore.save(newCaptureDevice);

            return newCaptureDevice;
        }

    }

    public void createFlowEntries(String captureDevice) {
        this.createOrUpdateCaptureDevice(captureDevice);
    }

}