This is an archive of the ONOS 1.2 wiki. For the current ONOS wiki, look here.

Overview

The following tutorial describes the bare minimum required for an ONOS app to define and register a custom UI view, which will be injected into the ONOS GUI whenever the app is loaded.

(Note: we are currently working on simplifying this process by using Maven Archetypes. Watch this space for updates...)

Project Directory Layout

The project directory structure should look similar to the following illustration. Note that we will be creating the indicated UI specific resources:

AppComponent

The AppComponent class is the Java representation of your application. At activation, the application code should create the appropriate components and register them with the ONOS platform. At deactivation, the code should unregister components and do other necessary cleanup.

For this tutorial, we are going to create a UiExtension that defines a custom view to be injected into the GUI every time the application is installed and activated.

First of all, the skeletal AppComponent class should look something like this:

/**
 * Skeletal ONOS application component.
 */
@Component(immediate = true)
public class AppComponent {
 
    private static final Logger log = LoggerFactory.getLogger(AppComponent.class);
 
    @Activate
    protected void activate() {
        log.info("Started");
    }
 
    @Deactivate
    protected void deactivate() {
        log.info("Stopped");
    }
}

Injecting the UiExtensionService

Once we have defined our UiExtension, we are going to want to register it with the UiExtensionService. Thus we need to inject a reference to that service into our class. Note that our app can't really run without this, so we use the mandatory flag for the service:

@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected UiExtensionService uiExtensionService;

Defining the UiExtension

To create a UiExtension we need to supply a list of views that it defines, a factory that will generate our message handlers on demand, and a reference to the class loader. So let's take care of those things up front...

The class loader:

private static final ClassLoader CL = AppComponent.class.getClassLoader();

The message handler factory:

private final UiMessageHandlerFactory messageHandlerFactory =
        () -> ImmutableList.of(
                new MeowMessageHandler()
        );

This factory is called upon whenever we register our UiExtension with the extension service, to create instances of message handlers to be registered with the web-socket-based event system that the GUI uses to communicate with the server-side code. We will define the private inner class MeowMessageHandler in a little bit...

The list of UiViews:

private final List<UiView> uiViews = ImmutableList.of(
        new UiView(UiView.Category.OTHER, "meowStuff", "Meowster Stuff")
);

Note that our tutorial is only defining one custom view. However, a UiExtension can provide more than one view by defining each one in this list (and writing the supporting client-side code, obviously).

The first argument to the UiView constructor is the navigation category to which the view should be added. Currently defined categories are "Platform", "Network", and "Other". (Well, okay, there is also "Hidden", but views in this category don't show up in the navigation menu).

The second argument to the constructor ("meowStuff") is the internal identifier that will be used for this view. This identifier is used in several key places in the code; choose it well.

 As a side note, in order to reduce the likelihood of ID collisions, we recommend that you pick a short word specific to your app as a prefix, and then provide a unique suffix for each view. In the tutorial here, "meow" is the prefix for all the Meowster Inc. views, and the individual views have IDs such as meowStuff, meowFoo, meowBar, etc.

The third argument is the text to display for the navigation item in the navigation menu.

With these three pieces defined, we can now construct the extension:

protected UiExtension extension =
        new UiExtension(uiViews, messageHandlerFactory, CL);

Registering the UiExtension

We need to register and unregister the extension at the appropriate times. Let's modify the activate() and deactivate() methods appropriately:

@Activate
protected void activate() {
    uiExtensionService.register(extension);
    log.info("Started Meowster App");
}
 
@Deactivate
protected void deactivate() {
    uiExtensionService.unregister(extension);
    log.info("Stopped Meowster App");
}

The Message Handler

The final piece to the puzzle is to write our message handler. For now we'll write the minimal code necessary; later we'll expand it to do something useful.

When a message handler is constructed, we tell the parent class which "event types" our handler supports by providing a list of strings. We have to provide at least one string, but may define many. In our example here, we are declaring that any events of type "meowRequest" should be routed to our handler.

On receipt of an event (matching one of the types declared) our process() method is invoked. It is passed a JSON ObjectNode representing the event. Our code can process the event and send back an appropriate response. For now, since we are simply working on bootstrapping a custom view, we will just log that we got the event.

private class MeowMessageHandler extends UiMessageHandler {
    protected MeowMessageHandler() {
        super(ImmutableSet.of("meowRequest"));
    }
 
    @Override
    public void process(ObjectNode objectNode) {
        log.info("we got a message: {}", objectNode);
    }
}

Putting it all together

Our AppComponent class should now look something like this:

package com.meowster.onos.app;
 
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.onosproject.ui.UiExtension;
import org.onosproject.ui.UiExtensionService;
import org.onosproject.ui.UiMessageHandler;
import org.onosproject.ui.UiMessageHandlerFactory;
import org.onosproject.ui.UiView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.List;
 
/**
 * Skeletal ONOS application component.
 */
@Component(immediate = true)
public class AppComponent {
 
    private static final Logger log = LoggerFactory.getLogger(AppComponent.class);
    private static final ClassLoader CL = AppComponent.class.getClassLoader();
 
    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected UiExtensionService uiExtensionService;
 
 
    private final UiMessageHandlerFactory messageHandlerFactory =
            () -> ImmutableList.of(
                    new MeowMessageHandler()
            );
 
    private final List<UiView> uiViews = ImmutableList.of(
            new UiView(UiView.Category.OTHER, "meowStuff", "Meowster Stuff")
    );
 
    protected UiExtension extension =
            new UiExtension(uiViews, messageHandlerFactory, CL);
 
 
    @Activate
    protected void activate() {
        uiExtensionService.register(extension);
        log.info("Started Meowster App");
    }
 
    @Deactivate
    protected void deactivate() {
        uiExtensionService.unregister(extension);
        log.info("Stopped Meowster App");
    }
 
 
    private class MeowMessageHandler extends UiMessageHandler {
        protected MeowMessageHandler() {
            super(ImmutableSet.of("meowRequest"));
        }
 
        @Override
        public void process(ObjectNode objectNode) {
            log.info("we got a message: {}", objectNode);
        }
    }
}

 

Client-Side Code

We now need to write client-side code (an HTML partial, CSS, and JavaScript) to define our view. These resources will be downloaded to the GUI when the browser window is refreshed, and utilized when our view is selected from the navigation menu.

Note that the ONOS GUI uses AngularJS as the underlying framework. This means that:

  • we define an HTML partial file to provide the template for our view
  • we define the view's controller (in the JavaScript file) in its own Angular "Module"

View Source Directory

Looking back at the directory structure, you will see the following directory path:

  • src/main/resources/app/view

In this directory there should be a subdirectory for every view that our UiExtension defines; the name of the directory should be the internal identifier used for that view. In our tutorial, this is the following directory:

  • src/main/resources/app/view/meowStuff

We define our HTML partial, CSS, and JavaScript resources in this directory: meowStuff.htmlmeowStuff.css and meowStuff.js.

View HTML Partial File

We define a template for the view in an HTML partial file: meowStuff.html...

<!-- partial HTML -->
<div id="ov-meowStuff">
    <h2> Meowster Inc. -- Useful Stuff </h2>

    <p> {{ctrl.msg}} </p>
</div>

Notes:

  • The view should have a top-level DIV element which has an "id" attribute of "ov-" + <view-id>
    • In our example, this is "ov-meowStuff". (Note that the "ov" simply stands for "ONOS View").
  • The controller for the view will be represented by the variable ctrl, which we can use with the usual Angular evaluation syntax.
    • Here, we are displaying a text message stored in the msg variable of the controller.

View CSS File

Our CSS rules will go in the file meowStuff.css. In this example, we provide style rules for paragraph elements:

/* css for a cat */

#ov-meowStuff p {
    margin: 0 30px;
    padding: 10px;
    border: 2px solid;
}

.light #ov-meowStuff p {
    color: darkblue;
    border-color: #88c;
}
.dark #ov-meowStuff p {
    color: #aac;
    border-color: #448;
}

Notes:

  • All our rules should have selectors that specify the ID of our outer div; in our example, this is #ov-meowStuff
    • we do this so that our rules only affect elements in our view
  • Color-related styles should be placed in separate rules and use the "theme" class prefixes of .light and .dark
  • Remember, the user can toggle between light and dark themes by pressing the 'T' key.

View JavaScript File

The JavaScript file (meowStuff.js) defines our angular "module" and the controller for the view. The structure of the file should be something like this:

// js for a cat
(function () {
    'use strict';

    angular.module('ovMeowStuff', [])
    .controller('OvMeowStuffCtrl',
        ['$log', '$scope',

        function ($log, $scope) {
            var self = this;

            self.msg = 'A message from our sponsors...';

            $log.log('OvMeowStuffCtrl has been created');
        }]);
}());

Notes:

  • We are using a function closure to provide encapsulation of our code (and not pollute the global namespace) – lines 2 and 16
  • Use strict mode to write safer code – line 3
  • Define our module as "ovMeowStuff" – line 5
    • This has to be "ov" + (capitalized)<view-id>"
  • Define our controller as "OvMeowStuffCtrl" – line 6
    • This has to be "Ov" + (capitalized)<view-id> + "Ctrl"
  • Note we are using the "safe" injection syntax for the controller parameters – lines 7 and 9
  • We capture the controller reference as the variable self line 10
    • A safeguard, in case the this context changes
  • We store a message in the controller's state – line 12
  • We write a message to the console – line 14

If little or none of this makes sense to you, then you probably need to go and study up on AngularJS controllers.

The reason that the module and controller names have to be exactly as specified above is because the main GUI controller derives these names from the internal identifier you supplied when you defined your UiView instance in your AppComponent class, to be able to stitch your view into the navigation and routing functions.

File Include Snippets

The final pieces of glue required are the two files css.html and js.html, which should both be at the top level of the resources directory. The contents of these files are spliced into the main index.html file of the GUI (on the fly) so that your resources are loaded into the browser.

For our tutorial example, css.html looks like this:

<link rel="stylesheet" href="app/view/meowStuff/meowStuff.css">

...and js.html looks like this:

<script src="app/view/meowStuff/meowStuff.js"></script>

And that's it... we should now be able to build and install our app, and see the view injected into the GUI.

Building and Installing the App

From the top level directory (onos-app) we can build the application with the command:

  • mvn clean install

or, if you have the shell tools and aliases sourced in...

  • mci

Fixing the Unit Test

When you first build the app, you will (probably) find that the build fails because of a broken unit test. The unit test skeleton (AppComponentTest.java) provided by the Maven archetype looks like this:

/**
 * Set of tests of the ONOS application component.
 */
public class AppComponentTest {

    private AppComponent component;

    @Before
    public void setUp() {
        component = new AppComponent();
        component.activate();
    }

    @After
    public void tearDown() {
        component.deactivate();
    }

    @Test
    public void basics() {

    }
}

The problem is in our AppComponent's activate() method (invoked on line 11 above) where the component's reference to uiExtensionService is null.

To fix this we need to provide the component with a mock service. We can create one from an appropriate adapter:

private UiExtensionService mockService = new UiExtensionServiceAdapter();

... and then inject it into the component during the setUp() method:

@Before
public void setUp() {
    component = new AppComponent();
    component.uiExtensionService = mockService;
    component.activate();
}

This will at least allow us to build successfully. 

Of course, since the unit test code doesn't actually assert anything, it isn't really a unit test yet... but let's not quibble over semantics for now. We'll leave it as an exercise for the reader to figure out how to make the unit test actually do something useful, such as asserting expected behavior of the AppComponent.

Go on... run 'mci' again... you know you want to. The output should bear a resemblance to this:

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building onos-app 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ onos-app ---
[INFO] Deleting /Users/simonh/dev/foo/onos-app/target
[INFO]
[INFO] --- onos-maven-plugin:1.4-SNAPSHOT:cfg (cfg) @ onos-app ---
[INFO] Generating ONOS component configuration catalogues...
[INFO]
[INFO] --- maven-resources-plugin:2.7:resources (default-resources) @ onos-app ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 5 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.5.1:compile (default-compile) @ onos-app ---
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /Users/simonh/dev/foo/onos-app/target/classes
[INFO]
[INFO] --- maven-scr-plugin:1.20.0:scr (generate-scr-srcdescriptor) @ onos-app ---
[INFO] Writing 1 Service Component Descriptors to /Users/simonh/dev/foo/onos-app/target/classes/OSGI-INF/com.meowster.onos.app.AppComponent.xml
[INFO]
[INFO] --- maven-resources-plugin:2.7:testResources (default-testResources) @ onos-app ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /Users/simonh/dev/foo/onos-app/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:2.5.1:testCompile (default-testCompile) @ onos-app ---
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /Users/simonh/dev/foo/onos-app/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.18:test (default-test) @ onos-app ---
[INFO] Surefire report directory: /Users/simonh/dev/foo/onos-app/target/surefire-reports
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.meowster.onos.app.AppComponentTest
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.07 sec - in com.meowster.onos.app.AppComponentTest
Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] --- maven-bundle-plugin:2.5.3:bundle (default-bundle) @ onos-app ---
[INFO]
[INFO] --- onos-maven-plugin:1.4-SNAPSHOT:app (app) @ onos-app ---
[INFO] Building ONOS application package for com.meowster.foo (v1.0-SNAPSHOT)
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ onos-app ---
[INFO] Installing /Users/simonh/dev/foo/onos-app/target/onos-app-1.0-SNAPSHOT.jar to /Users/simonh/.m2/repository/com/meowster/onos/app/onos-app/1.0-SNAPSHOT/onos-app-1.0-SNAPSHOT.jar
[INFO] Installing /Users/simonh/dev/foo/onos-app/pom.xml to /Users/simonh/.m2/repository/com/meowster/onos/app/onos-app/1.0-SNAPSHOT/onos-app-1.0-SNAPSHOT.pom
[INFO] Installing /Users/simonh/dev/foo/onos-app/target/onos-app-1.0-SNAPSHOT.oar to /Users/simonh/.m2/repository/com/meowster/onos/app/onos-app/1.0-SNAPSHOT/onos-app-1.0-SNAPSHOT.oar
[INFO]
[INFO] --- maven-bundle-plugin:2.5.3:install (default-install) @ onos-app ---
[INFO] Installing com/meowster/onos/app/onos-app/1.0-SNAPSHOT/onos-app-1.0-SNAPSHOT.jar
[INFO] Writing OBR metadata
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.698 s
[INFO] Finished at: 2015-04-29T14:48:14-07:00
[INFO] Final Memory: 24M/313M
[INFO] ------------------------------------------------------------------------

An important line to note from the above output is this one:

[INFO] Installing /Users/simonh/dev/foo/onos-app/target/onos-app-1.0-SNAPSHOT.oar to /Users/simonh/.m2/repository/com/meowster/onos/app/onos-app/1.0-SNAPSHOT/onos-app-1.0-SNAPSHOT.oar

Let's define an environment variable for the OAR file that was built. We can use the one from the target directory:

OAR=target/onos-app-1.0-SNAPSHOT.oar

We can also define an environment variable for the application name:

APPNAME=com.meowster.foo

Note: this was the name given during the Maven archetype "interview".

Installing the App for the First Time

We are going to use the onos-app shell tool. Its usage looks like this:

usage: onos-app <node-ip> list
       onos-app <node-ip> {install|install!} <app-file>
       onos-app <node-ip> {reinstall|reinstall!} <app-name> <app-file>
       onos-app <node-ip> {activate|deactivate|uninstall} <app-name>

Assuming that there is an ONOS instance running on localhost, we can install the application with the command:

onos-app localhost install $OAR

If you are tailing the karaf log, you should see an entry that looks like this:

<...> | 76 - org.onosproject.onos-core-net - 1.2.0.SNAPSHOT | Application com.meowster.foo has been installed

Opening the Applications view in the GUI should present you with something like this:

Now we can activate the application with the command:

onos-app localhost activate $APPNAME

The karaf log should have entries that look similar to this:

<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature onos-app 1.0-SNAPSHOT
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature onos-api 1.2.0-SNAPSHOT
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature scr 3.0.3
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature onos-thirdparty-base 1.2.0-SNAPSHOT
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature scr-condition-management_0_0_0 3.0.3
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature scr-condition-webconsole_0_0_0 3.0.3
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature webconsole-condition-scr_0_0_0 3.0.3
<...> | 20 - org.apache.karaf.features.core - 3.0.3 | Installing feature standard-condition-webconsole_0_0_0 3.0.3
<...> | 179 - com.meowster.onos.app.onos-app - 1.0.0.SNAPSHOT | Started Meowster App
<...> | 76 - org.onosproject.onos-core-net - 1.2.0.SNAPSHOT | Application com.meowster.foo has been activated

Note (line 9) the log message from our application's activate() method.

Refreshing the Applications view in the GUI should show a checkmark next to the Meowster app, indicating that it is active:

Now check out that navigation menu:

And click on the "Meowster Stuff" menu item, to load our custom view:

Congratulations!! You have successfully integrated a custom view for your ONOS application, into the ONOS GUI.

Application Development Cycle

Obviously, there will be a development cycle as you incrementally enhance your view's functionality:

  • modify code
  • re-build
  • re-install and activate application
  • refresh GUI
  • test behavior

After you have modified the code and rebuilt (with mci), you can re-install and activate the application all in one command:

onos-app localhost reinstall! $APPNAME $OAR

Note: the exclamation point ('!')  after reinstall means "...and activate".

What Next?

As stated at the beginning, this page describes the bare minimum required to get a custom view bootstrapped into the GUI. There are further refinements and details which can be applied. For further information you might like to peruse the following tutorials:

TutorialDescription

UI View - Sending and Receiving Events

How to communicate with your server-side AppComponent, via message handlers
UI View - Defining Custom IconsHow to add custom glyphs to the glyph library
UI View - Defining Key BindingsHow to bind keystrokes to actions (javascript callback functions)
UI View - Using Fly-In panelsHow to use the Panel Service to customize "fly-in" panels

UI View - Basic Tabular View

How to create a simple tabular view using the TableBuilderService
UI View - Framework LibrariesA summary of the services that the GUI framework provides

 

 

 

  • No labels