Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/apps/metrics/intent/pom.xml b/apps/metrics/intent/pom.xml
new file mode 100644
index 0000000..4d42065
--- /dev/null
+++ b/apps/metrics/intent/pom.xml
@@ -0,0 +1,32 @@
+<?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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.onlab.onos</groupId>
+    <artifactId>onos-app-metrics</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>onos-app-metrics-intent</artifactId>
+  <packaging>bundle</packaging>
+
+  <description>ONOS intent metrics application</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onos-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.karaf.shell</groupId>
+      <artifactId>org.apache.karaf.shell.console</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java
new file mode 100644
index 0000000..38366b6
--- /dev/null
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java
@@ -0,0 +1,319 @@
+package org.onlab.onos.metrics.intent;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.google.common.collect.ImmutableList;
+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.apache.felix.scr.annotations.Service;
+import org.onlab.metrics.MetricsComponent;
+import org.onlab.metrics.MetricsFeature;
+import org.onlab.metrics.MetricsService;
+import org.onlab.onos.net.intent.IntentEvent;
+import org.onlab.onos.net.intent.IntentListener;
+import org.onlab.onos.net.intent.IntentService;
+import org.slf4j.Logger;
+
+/**
+ * ONOS Intent Metrics Application that collects intent-related metrics.
+ */
+@Component(immediate = true)
+@Service
+public class IntentMetrics implements IntentMetricsService,
+                                      IntentListener {
+    private static final Logger log = getLogger(IntentMetrics.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentService intentService;
+    private LinkedList<IntentEvent> lastEvents = new LinkedList<>();
+    private static final int LAST_EVENTS_MAX_N = 100;
+
+    //
+    // Metrics
+    //
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MetricsService metricsService;
+    //
+    private static final String COMPONENT_NAME = "Intent";
+    private static final String FEATURE_SUBMITTED_NAME = "Submitted";
+    private static final String FEATURE_INSTALLED_NAME = "Installed";
+    private static final String FEATURE_WITHDRAW_REQUESTED_NAME =
+        "WithdrawRequested";
+    private static final String FEATURE_WITHDRAWN_NAME = "Withdrawn";
+    private static final String GAUGE_TIMESTAMP_NAME = "Timestamp.EpochMs";
+    private static final String METER_RATE_NAME = "Rate";
+    //
+    private MetricsComponent metricsComponent;
+    private MetricsFeature metricsFeatureSubmitted;
+    private MetricsFeature metricsFeatureInstalled;
+    private MetricsFeature metricsFeatureWithdrawRequested;
+    private MetricsFeature metricsFeatureWithdrawn;
+    //
+    // Timestamps:
+    //  - Intent Submitted API operation (ms from the Epoch)
+    //  - Intent Installed operation completion (ms from the Epoch)
+    //  - Intent Withdraw Requested API operation (ms from the Epoch)
+    //  - Intent Withdrawn operation completion (ms from the Epoch)
+    //
+    private volatile long intentSubmittedTimestampEpochMs = 0;
+    private volatile long intentInstalledTimestampEpochMs = 0;
+    private volatile long intentWithdrawRequestedTimestampEpochMs = 0;
+    private volatile long intentWithdrawnTimestampEpochMs = 0;
+    //
+    private Gauge<Long> intentSubmittedTimestampEpochMsGauge;
+    private Gauge<Long> intentInstalledTimestampEpochMsGauge;
+    private Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge;
+    private Gauge<Long> intentWithdrawnTimestampEpochMsGauge;
+    //
+    // Rate meters:
+    //  - Rate of the Submitted Intent API operations
+    //  - Rate of the Installed Intent operations
+    //  - Rate of the Withdrawn Requested Intent API operations
+    //  - Rate of the Withdrawn Intent operations
+    //
+    private Meter intentSubmittedRateMeter;
+    private Meter intentInstalledRateMeter;
+    private Meter intentWithdrawRequestedRateMeter;
+    private Meter intentWithdrawnRateMeter;
+
+    @Activate
+    protected void activate() {
+        clear();
+        registerMetrics();
+        intentService.addListener(this);
+        log.info("ONOS Intent Metrics started.");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        intentService.removeListener(this);
+        removeMetrics();
+        clear();
+        log.info("ONOS Intent Metrics stopped.");
+    }
+
+    @Override
+    public List<IntentEvent> getEvents() {
+        synchronized (lastEvents) {
+            return ImmutableList.<IntentEvent>copyOf(lastEvents);
+        }
+    }
+
+    @Override
+    public Gauge<Long> intentSubmittedTimestampEpochMsGauge() {
+        return intentSubmittedTimestampEpochMsGauge;
+    }
+
+    @Override
+    public Gauge<Long> intentInstalledTimestampEpochMsGauge() {
+        return intentInstalledTimestampEpochMsGauge;
+    }
+
+    @Override
+    public Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge() {
+        return intentWithdrawRequestedTimestampEpochMsGauge;
+    }
+
+    @Override
+    public Gauge<Long> intentWithdrawnTimestampEpochMsGauge() {
+        return intentWithdrawnTimestampEpochMsGauge;
+    }
+
+    @Override
+    public Meter intentSubmittedRateMeter() {
+        return intentSubmittedRateMeter;
+    }
+
+    @Override
+    public Meter intentInstalledRateMeter() {
+        return intentInstalledRateMeter;
+    }
+
+    @Override
+    public Meter intentWithdrawRequestedRateMeter() {
+        return intentWithdrawRequestedRateMeter;
+    }
+
+    @Override
+    public Meter intentWithdrawnRateMeter() {
+        return intentWithdrawnRateMeter;
+    }
+
+    @Override
+    public void event(IntentEvent event) {
+        synchronized (lastEvents) {
+            //
+            // TODO: The processing below is incomplete: we don't have
+            // an event equivalent of "Withdraw Requested"
+            //
+            switch (event.type()) {
+            case SUBMITTED:
+                intentSubmittedTimestampEpochMs = System.currentTimeMillis();
+                intentSubmittedRateMeter.mark(1);
+                break;
+            case INSTALLED:
+                intentInstalledTimestampEpochMs = System.currentTimeMillis();
+                intentInstalledRateMeter.mark(1);
+                break;
+            case FAILED:
+                // TODO: Just ignore?
+                break;
+                /*
+            case WITHDRAW_REQUESTED:
+                intentWithdrawRequestedTimestampEpochMs =
+                    System.currentTimeMillis();
+                    intentWithdrawRequestedRateMeter.mark(1);
+                break;
+                */
+            case WITHDRAWN:
+                intentWithdrawnTimestampEpochMs = System.currentTimeMillis();
+                intentWithdrawnRateMeter.mark(1);
+                break;
+            default:
+                break;
+            }
+
+            //
+            // Keep only the last N events, where N = LAST_EVENTS_MAX_N
+            //
+            while (lastEvents.size() >= LAST_EVENTS_MAX_N) {
+                lastEvents.remove();
+            }
+            lastEvents.add(event);
+        }
+
+        log.debug("Intent Event: time = {} type = {} event = {}",
+                  event.time(), event.type(), event);
+    }
+
+    /**
+     * Clears the internal state.
+     */
+    private void clear() {
+        synchronized (lastEvents) {
+            intentSubmittedTimestampEpochMs = 0;
+            intentInstalledTimestampEpochMs = 0;
+            intentWithdrawRequestedTimestampEpochMs = 0;
+            intentWithdrawnTimestampEpochMs = 0;
+            lastEvents.clear();
+        }
+    }
+
+    /**
+     * Registers the metrics.
+     */
+    private void registerMetrics() {
+        metricsComponent = metricsService.registerComponent(COMPONENT_NAME);
+        //
+        metricsFeatureSubmitted =
+            metricsComponent.registerFeature(FEATURE_SUBMITTED_NAME);
+        metricsFeatureInstalled =
+            metricsComponent.registerFeature(FEATURE_INSTALLED_NAME);
+        metricsFeatureWithdrawRequested =
+            metricsComponent.registerFeature(FEATURE_WITHDRAW_REQUESTED_NAME);
+        metricsFeatureWithdrawn =
+            metricsComponent.registerFeature(FEATURE_WITHDRAWN_NAME);
+        //
+        intentSubmittedTimestampEpochMsGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeatureSubmitted,
+                                          GAUGE_TIMESTAMP_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return intentSubmittedTimestampEpochMs;
+                                              }
+                                          });
+        //
+        intentInstalledTimestampEpochMsGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeatureInstalled,
+                                          GAUGE_TIMESTAMP_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return intentInstalledTimestampEpochMs;
+                                              }
+                                          });
+        //
+        intentWithdrawRequestedTimestampEpochMsGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeatureWithdrawRequested,
+                                          GAUGE_TIMESTAMP_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return intentWithdrawRequestedTimestampEpochMs;
+                                              }
+                                          });
+        //
+        intentWithdrawnTimestampEpochMsGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeatureWithdrawn,
+                                          GAUGE_TIMESTAMP_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return intentWithdrawnTimestampEpochMs;
+                                              }
+                                          });
+        //
+        intentSubmittedRateMeter =
+            metricsService.createMeter(metricsComponent,
+                                       metricsFeatureSubmitted,
+                                       METER_RATE_NAME);
+        //
+        intentInstalledRateMeter =
+            metricsService.createMeter(metricsComponent,
+                                       metricsFeatureInstalled,
+                                       METER_RATE_NAME);
+        //
+        intentWithdrawRequestedRateMeter =
+            metricsService.createMeter(metricsComponent,
+                                       metricsFeatureWithdrawRequested,
+                                       METER_RATE_NAME);
+        //
+        intentWithdrawnRateMeter =
+            metricsService.createMeter(metricsComponent,
+                                       metricsFeatureWithdrawn,
+                                       METER_RATE_NAME);
+    }
+
+    /**
+     * Removes the metrics.
+     */
+    private void removeMetrics() {
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureSubmitted,
+                                    GAUGE_TIMESTAMP_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureInstalled,
+                                    GAUGE_TIMESTAMP_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureWithdrawRequested,
+                                    GAUGE_TIMESTAMP_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureWithdrawn,
+                                    GAUGE_TIMESTAMP_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureSubmitted,
+                                    METER_RATE_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureInstalled,
+                                    METER_RATE_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureWithdrawRequested,
+                                    METER_RATE_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeatureWithdrawn,
+                                    METER_RATE_NAME);
+    }
+}
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java
new file mode 100644
index 0000000..4acd00f
--- /dev/null
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java
@@ -0,0 +1,85 @@
+package org.onlab.onos.metrics.intent;
+
+import java.util.List;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import org.onlab.onos.net.intent.IntentEvent;
+
+/**
+ * Service interface exported by IntentMetrics.
+ */
+public interface IntentMetricsService {
+    /**
+     * Gets the last saved intent events.
+     *
+     * @return the last saved intent events.
+     */
+    public List<IntentEvent> getEvents();
+
+    /**
+     * Gets the Metrics' Gauge for the intent SUBMITTED event timestamp
+     * (ms from the epoch).
+     *
+     * @return the Metrics' Gauge for the intent SUBMITTED event timestamp
+     * (ms from the epoch)
+     */
+    public Gauge<Long> intentSubmittedTimestampEpochMsGauge();
+
+    /**
+     * Gets the Metrics' Gauge for the intent INSTALLED event timestamp
+     * (ms from the epoch).
+     *
+     * @return the Metrics' Gauge for the intent INSTALLED event timestamp
+     * (ms from the epoch)
+     */
+    public Gauge<Long> intentInstalledTimestampEpochMsGauge();
+
+    /**
+     * Gets the Metrics' Gauge for the intent WITHDRAW_REQUESTED event
+     * timestamp (ms from the epoch).
+     *
+     * TODO: This intent event is not implemented yet.
+     *
+     * @return the Metrics' Gauge for the intent WITHDRAW_REQUESTED event
+     * timestamp (ms from the epoch)
+     */
+    public Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge();
+
+    /**
+     * Gets the Metrics' Gauge for the intent WITHDRAWN event timestamp
+     * (ms from the epoch).
+     *
+     * @return the Metrics' Gauge for the intent WITHDRAWN event timestamp
+     * (ms from the epoch)
+     */
+    public Gauge<Long> intentWithdrawnTimestampEpochMsGauge();
+
+    /**
+     * Gets the Metrics' Meter for the submitted intents event rate.
+     *
+     * @return the Metrics' Meter for the submitted intents event rate
+     */
+    public Meter intentSubmittedRateMeter();
+
+    /**
+     * Gets the Metrics' Meter for the installed intents event rate.
+     *
+     * @return the Metrics' Meter for the installed intent event rate
+     */
+    public Meter intentInstalledRateMeter();
+
+    /**
+     * Gets the Metrics' Meter for the withdraw requested intents event rate.
+     *
+     * @return the Metrics' Meter for the withdraw requested intents event rate
+     */
+    public Meter intentWithdrawRequestedRateMeter();
+
+    /**
+     * Gets the Metrics' Meter for the withdraw completed intents event rate.
+     *
+     * @return the Metrics' Meter for the withdraw completed intents event rate
+     */
+    public Meter intentWithdrawnRateMeter();
+}
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsListCommand.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsListCommand.java
new file mode 100644
index 0000000..e07c3a5
--- /dev/null
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsListCommand.java
@@ -0,0 +1,68 @@
+package org.onlab.onos.metrics.intent.cli;
+
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.metrics.intent.IntentMetricsService;
+import org.onlab.onos.net.intent.IntentEvent;
+
+/**
+ * Command to show the list of last intent events.
+ */
+@Command(scope = "onos", name = "intents-events",
+         description = "Lists the last intent events")
+public class IntentEventsListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_EVENT = "Event=%s";
+
+    @Override
+    protected void execute() {
+        IntentMetricsService service = get(IntentMetricsService.class);
+
+        if (outputJson()) {
+            print("%s", json(service.getEvents()));
+        } else {
+            for (IntentEvent event : service.getEvents()) {
+                print(FORMAT_EVENT, event);
+                print("");          // Extra empty line for clarity
+            }
+        }
+    }
+
+    /**
+     * Produces a JSON array of intent events.
+     *
+     * @param intentEvents the intent events with the data
+     * @return JSON array with the intent events
+     */
+    private JsonNode json(List<IntentEvent> intentEvents) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+
+        for (IntentEvent event : intentEvents) {
+            result.add(json(mapper, event));
+        }
+        return result;
+    }
+
+    /**
+     * Produces JSON object for a intent event.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param intentEvent the intent event with the data
+     * @return JSON object for the intent event
+     */
+    private ObjectNode json(ObjectMapper mapper, IntentEvent intentEvent) {
+        ObjectNode result = mapper.createObjectNode();
+
+        result.put("time", intentEvent.time())
+            .put("type", intentEvent.type().toString())
+            .put("event", intentEvent.toString());
+        return result;
+    }
+}
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java
new file mode 100644
index 0000000..204cfd6
--- /dev/null
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java
@@ -0,0 +1,132 @@
+package org.onlab.onos.metrics.intent.cli;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.json.MetricsModule;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.metrics.intent.IntentMetricsService;
+
+/**
+ * Command to show the intent events metrics.
+ */
+@Command(scope = "onos", name = "intents-events-metrics",
+         description = "Lists intent events metrics")
+public class IntentEventsMetricsCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_GAUGE =
+        "Intent %s Event Timestamp (ms from epoch)=%d";
+    private static final String FORMAT_METER =
+        "Intent %s Events count=%d rate(events/sec) mean=%f m1=%f m5=%f m15=%f";
+
+    @Override
+    protected void execute() {
+        IntentMetricsService service = get(IntentMetricsService.class);
+        Gauge<Long> gauge;
+        Meter meter;
+
+        if (outputJson()) {
+            ObjectMapper mapper = new ObjectMapper()
+                .registerModule(new MetricsModule(TimeUnit.SECONDS,
+                                                  TimeUnit.MILLISECONDS,
+                                                  false));
+            ObjectNode result = mapper.createObjectNode();
+            //
+            gauge = service.intentSubmittedTimestampEpochMsGauge();
+            result.put("intentSubmittedTimestamp", json(mapper, gauge));
+            gauge = service.intentInstalledTimestampEpochMsGauge();
+            result.put("intentInstalledTimestamp", json(mapper, gauge));
+            gauge = service.intentWithdrawRequestedTimestampEpochMsGauge();
+            result.put("intentWithdrawRequestedTimestamp",
+                       json(mapper, gauge));
+            gauge = service.intentWithdrawnTimestampEpochMsGauge();
+            result.put("intentWithdrawnTimestamp", json(mapper, gauge));
+            //
+            meter = service.intentSubmittedRateMeter();
+            result.put("intentSubmittedRate", json(mapper, meter));
+            meter = service.intentInstalledRateMeter();
+            result.put("intentInstalledRate", json(mapper, meter));
+            meter = service.intentWithdrawRequestedRateMeter();
+            result.put("intentWithdrawRequestedRate", json(mapper, meter));
+            meter = service.intentWithdrawnRateMeter();
+            result.put("intentWithdrawnRate", json(mapper, meter));
+            //
+            print("%s", result);
+        } else {
+            gauge = service.intentSubmittedTimestampEpochMsGauge();
+            printGauge("Submitted", gauge);
+            gauge = service.intentInstalledTimestampEpochMsGauge();
+            printGauge("Installed", gauge);
+            gauge = service.intentWithdrawRequestedTimestampEpochMsGauge();
+            printGauge("Withdraw Requested", gauge);
+            gauge = service.intentWithdrawnTimestampEpochMsGauge();
+            printGauge("Withdrawn", gauge);
+            //
+            meter = service.intentSubmittedRateMeter();
+            printMeter("Submitted", meter);
+            meter = service.intentInstalledRateMeter();
+            printMeter("Installed", meter);
+            meter = service.intentWithdrawRequestedRateMeter();
+            printMeter("Withdraw Requested", meter);
+            meter = service.intentWithdrawnRateMeter();
+            printMeter("Withdrawn", meter);
+        }
+    }
+
+    /**
+     * Produces JSON node for an Object.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param object the Object with the data
+     * @return JSON node for the Object
+     */
+    private JsonNode json(ObjectMapper mapper, Object object) {
+        //
+        // NOTE: The API for custom serializers is incomplete,
+        // hence we have to parse the JSON string to create JsonNode.
+        //
+        try {
+            final String objectJson = mapper.writeValueAsString(object);
+            JsonNode objectNode = mapper.readTree(objectJson);
+            return objectNode;
+        } catch (JsonProcessingException e) {
+            log.error("Error writing value as JSON string", e);
+        } catch (IOException e) {
+            log.error("Error writing value as JSON string", e);
+        }
+        return null;
+    }
+
+    /**
+     * Prints a Gauge.
+     *
+     * @param operationStr the string with the intent operation to print
+     * @param gauge the Gauge to print
+     */
+    private void printGauge(String operationStr, Gauge<Long> gauge) {
+        print(FORMAT_GAUGE, operationStr, gauge.getValue());
+    }
+
+    /**
+     * Prints a Meter.
+     *
+     * @param operationStr the string with the intent operation to print
+     * @param meter the Meter to print
+     */
+    private void printMeter(String operationStr, Meter meter) {
+            TimeUnit rateUnit = TimeUnit.SECONDS;
+            double rateFactor = rateUnit.toSeconds(1);
+            print(FORMAT_METER, operationStr, meter.getCount(),
+                  meter.getMeanRate() * rateFactor,
+                  meter.getOneMinuteRate() * rateFactor,
+                  meter.getFiveMinuteRate() * rateFactor,
+                  meter.getFifteenMinuteRate() * rateFactor);
+    }
+}
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/package-info.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/package-info.java
new file mode 100644
index 0000000..5bba126
--- /dev/null
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * ONOS Intent Metrics Application that collects intent-related metrics.
+ */
+package org.onlab.onos.metrics.intent;
diff --git a/apps/metrics/intent/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/metrics/intent/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..04f179a
--- /dev/null
+++ b/apps/metrics/intent/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,12 @@
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+  <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+    <command>
+      <action class="org.onlab.onos.metrics.intent.cli.IntentEventsListCommand"/>
+    </command>
+    <command>
+      <action class="org.onlab.onos.metrics.intent.cli.IntentEventsMetricsCommand"/>
+    </command>
+  </command-bundle>
+
+</blueprint>
diff --git a/apps/metrics/pom.xml b/apps/metrics/pom.xml
index 0ce3913..67085c2 100644
--- a/apps/metrics/pom.xml
+++ b/apps/metrics/pom.xml
@@ -17,6 +17,7 @@
   <description>ONOS metrics applications</description>
 
   <modules>
+    <module>intent</module>
     <module>topology</module>
   </modules>
 
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
index e2a4532..32cf0cf 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
@@ -18,6 +18,15 @@
 import org.onlab.metrics.MetricsFeature;
 import org.onlab.metrics.MetricsService;
 import org.onlab.onos.event.Event;
+import org.onlab.onos.net.device.DeviceEvent;
+import org.onlab.onos.net.device.DeviceListener;
+import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.host.HostEvent;
+import org.onlab.onos.net.host.HostListener;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.link.LinkEvent;
+import org.onlab.onos.net.link.LinkListener;
+import org.onlab.onos.net.link.LinkService;
 import org.onlab.onos.net.topology.TopologyEvent;
 import org.onlab.onos.net.topology.TopologyListener;
 import org.onlab.onos.net.topology.TopologyService;
@@ -28,14 +37,26 @@
  */
 @Component(immediate = true)
 @Service
-public class TopologyMetrics implements TopologyMetricsService,
-                                        TopologyListener {
+public class TopologyMetrics implements TopologyMetricsService {
     private static final Logger log = getLogger(TopologyMetrics.class);
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkService linkService;
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected TopologyService topologyService;
-    private LinkedList<TopologyEvent> lastEvents = new LinkedList<>();
-    private static final int LAST_EVENTS_MAX_N = 10;
+
+    private LinkedList<Event> lastEvents = new LinkedList<>();
+    private static final int LAST_EVENTS_MAX_N = 100;
+
+    private final DeviceListener deviceListener = new InnerDeviceListener();
+    private final HostListener hostListener = new InnerHostListener();
+    private final LinkListener linkListener = new InnerLinkListener();
+    private final TopologyListener topologyListener =
+        new InnerTopologyListener();
 
     //
     // Metrics
@@ -61,22 +82,33 @@
     protected void activate() {
         clear();
         registerMetrics();
-        topologyService.addListener(this);
+
+        // Register for all topology-related events
+        deviceService.addListener(deviceListener);
+        hostService.addListener(hostListener);
+        linkService.addListener(linkListener);
+        topologyService.addListener(topologyListener);
+
         log.info("ONOS Topology Metrics started.");
     }
 
     @Deactivate
     public void deactivate() {
-        topologyService.removeListener(this);
+        // De-register from all topology-related events
+        deviceService.removeListener(deviceListener);
+        hostService.removeListener(hostListener);
+        linkService.removeListener(linkListener);
+        topologyService.removeListener(topologyListener);
+
         removeMetrics();
         clear();
         log.info("ONOS Topology Metrics stopped.");
     }
 
     @Override
-    public List<TopologyEvent> getEvents() {
+    public List<Event> getEvents() {
         synchronized (lastEvents) {
-            return ImmutableList.<TopologyEvent>copyOf(lastEvents);
+            return ImmutableList.<Event>copyOf(lastEvents);
         }
     }
 
@@ -90,27 +122,22 @@
         return eventRateMeter;
     }
 
-    @Override
-    public void event(TopologyEvent event) {
-        lastEventTimestampEpochMs = System.currentTimeMillis();
-        //
-        // NOTE: If we want to count each "reason" as a separate event,
-        // then we should use 'event.reason().size()' instead of '1' to
-        // mark the meter below.
-        //
-        eventRateMeter.mark(1);
-
-        log.debug("Topology Event: time = {} type = {} subject = {}",
-                  event.time(), event.type(), event.subject());
-        for (Event reason : event.reasons()) {
-            log.debug("Topology Event Reason: time = {} type = {} subject = {}",
-                      reason.time(), reason.type(), reason.subject());
-        }
-
-        //
-        // Keep only the last N events, where N = LAST_EVENTS_MAX_N
-        //
+    /**
+     * Records an event.
+     *
+     * @param event the event to record
+     * @param updateEventRateMeter if true, update the Event Rate Meter
+     */
+    private void recordEvent(Event event, boolean updateEventRateMeter) {
         synchronized (lastEvents) {
+            lastEventTimestampEpochMs = System.currentTimeMillis();
+            if (updateEventRateMeter) {
+                eventRateMeter.mark(1);
+            }
+
+            //
+            // Keep only the last N events, where N = LAST_EVENTS_MAX_N
+            //
             while (lastEvents.size() >= LAST_EVENTS_MAX_N) {
                 lastEvents.remove();
             }
@@ -119,11 +146,67 @@
     }
 
     /**
+     * Inner Device Event Listener class.
+     */
+    private class InnerDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            recordEvent(event, true);
+            log.debug("Device Event: time = {} type = {} event = {}",
+                      event.time(), event.type(), event);
+        }
+    }
+
+    /**
+     * Inner Host Event Listener class.
+     */
+    private class InnerHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            recordEvent(event, true);
+            log.debug("Host Event: time = {} type = {} event = {}",
+                      event.time(), event.type(), event);
+        }
+    }
+
+    /**
+     * Inner Link Event Listener class.
+     */
+    private class InnerLinkListener implements LinkListener {
+        @Override
+        public void event(LinkEvent event) {
+            recordEvent(event, true);
+            log.debug("Link Event: time = {} type = {} event = {}",
+                      event.time(), event.type(), event);
+        }
+    }
+
+    /**
+     * Inner Topology Event Listener class.
+     */
+    private class InnerTopologyListener implements TopologyListener {
+        @Override
+        public void event(TopologyEvent event) {
+            //
+            // NOTE: Don't update the eventRateMeter, because the real
+            // events are already captured/counted.
+            //
+            recordEvent(event, false);
+            log.debug("Topology Event: time = {} type = {} event = {}",
+                      event.time(), event.type(), event);
+            for (Event reason : event.reasons()) {
+                log.debug("Topology Event Reason: time = {} type = {} event = {}",
+                          reason.time(), reason.type(), reason);
+            }
+        }
+    }
+
+    /**
      * Clears the internal state.
      */
     private void clear() {
-        lastEventTimestampEpochMs = 0;
         synchronized (lastEvents) {
+            lastEventTimestampEpochMs = 0;
             lastEvents.clear();
         }
     }
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
index cc370fa..aeb2e32 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
@@ -4,7 +4,7 @@
 
 import com.codahale.metrics.Gauge;
 import com.codahale.metrics.Meter;
-import org.onlab.onos.net.topology.TopologyEvent;
+import org.onlab.onos.event.Event;
 
 /**
  * Service interface exported by TopologyMetrics.
@@ -15,7 +15,7 @@
      *
      * @return the last saved topology events.
      */
-    public List<TopologyEvent> getEvents();
+    public List<Event> getEvents();
 
     /**
      * Gets the Metrics' Gauge for the last topology event timestamp
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java
index 8bab4d0..f8d0c1a 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsListCommand.java
@@ -19,10 +19,8 @@
          description = "Lists the last topology events")
 public class TopologyEventsListCommand extends AbstractShellCommand {
 
-    private static final String FORMAT_EVENT =
-        "Topology Event time=%d type=%s subject=%s";
-    private static final String FORMAT_REASON =
-        "    Reason time=%d type=%s subject=%s";
+    private static final String FORMAT_EVENT =  "Event=%s";
+    private static final String FORMAT_REASON = "    Reason=%s";
 
     @Override
     protected void execute() {
@@ -31,12 +29,13 @@
         if (outputJson()) {
             print("%s", json(service.getEvents()));
         } else {
-            for (TopologyEvent event : service.getEvents()) {
-                print(FORMAT_EVENT, event.time(), event.type(),
-                      event.subject());
-                for (Event reason : event.reasons()) {
-                    print(FORMAT_REASON, reason.time(), reason.type(),
-                          reason.subject());
+            for (Event event : service.getEvents()) {
+                print(FORMAT_EVENT, event);
+                if (event instanceof TopologyEvent) {
+                    TopologyEvent topologyEvent = (TopologyEvent) event;
+                    for (Event reason : topologyEvent.reasons()) {
+                        print(FORMAT_REASON, reason);
+                    }
                 }
                 print("");          // Extra empty line for clarity
             }
@@ -46,14 +45,14 @@
     /**
      * Produces a JSON array of topology events.
      *
-     * @param topologyEvents the topology events with the data
+     * @param events the topology events with the data
      * @return JSON array with the topology events
      */
-    private JsonNode json(List<TopologyEvent> topologyEvents) {
+    private JsonNode json(List<Event> events) {
         ObjectMapper mapper = new ObjectMapper();
         ArrayNode result = mapper.createArrayNode();
 
-        for (TopologyEvent event : topologyEvents) {
+        for (Event event : events) {
             result.add(json(mapper, event));
         }
         return result;
@@ -66,32 +65,23 @@
      * @param topologyEvent the topology event with the data
      * @return JSON object for the topology event
      */
-    private ObjectNode json(ObjectMapper mapper, TopologyEvent topologyEvent) {
-        ObjectNode result = mapper.createObjectNode();
-        ArrayNode reasons = mapper.createArrayNode();
-
-        for (Event reason : topologyEvent.reasons()) {
-            reasons.add(json(mapper, reason));
-        }
-        result.put("time", topologyEvent.time())
-            .put("type", topologyEvent.type().toString())
-            .put("subject", topologyEvent.subject().toString())
-            .put("reasons", reasons);
-        return result;
-    }
-
-    /**
-     * Produces JSON object for a generic event.
-     *
-     * @param event the generic event with the data
-     * @return JSON object for the generic event
-     */
     private ObjectNode json(ObjectMapper mapper, Event event) {
         ObjectNode result = mapper.createObjectNode();
 
         result.put("time", event.time())
             .put("type", event.type().toString())
-            .put("subject", event.subject().toString());
+            .put("event", event.toString());
+
+        // Add the reasons if a TopologyEvent
+        if (event instanceof TopologyEvent) {
+            TopologyEvent topologyEvent = (TopologyEvent) event;
+            ArrayNode reasons = mapper.createArrayNode();
+            for (Event reason : topologyEvent.reasons()) {
+                reasons.add(json(mapper, reason));
+            }
+            result.put("reasons", reasons);
+        }
+
         return result;
     }
 }
diff --git a/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java b/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
index b2e273f..cfdeb1f 100644
--- a/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
+++ b/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
@@ -284,7 +284,7 @@
             DefaultLinkDescription linkDescription =
                     new DefaultLinkDescription(srcPoint,
                                                  snkPoint,
-                                                 Link.Type.DIRECT,
+                                                 Link.Type.OPTICAL,
                                                  extendedAttributes);
 
             linkProviderService.linkDetected(linkDescription);
@@ -315,7 +315,7 @@
             DefaultLinkDescription linkDescription =
                     new DefaultLinkDescription(srcPoint,
                                                  snkPoint,
-                                                 Link.Type.DIRECT,
+                                                 Link.Type.OPTICAL,
                                                  extendedAttributes);
 
             linkProviderService.linkDetected(linkDescription);
diff --git a/apps/optical/src/main/resources/demo-3-roadm-2-ps.json b/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
index 6f2c2f5..20b7db2 100644
--- a/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
+++ b/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
@@ -1,5 +1,5 @@
 {
-        "opticalSwitches": [
+    "opticalSwitches": [
         {
             "allowed": true,
             "latitude": 37.6,
@@ -12,7 +12,7 @@
             "type": "Roadm"
         },
 
-	{
+        {
             "allowed": true,
             "latitude": 37.3,
             "longitude": 121.9,
@@ -22,9 +22,9 @@
                 "numRegen": 0
             },
             "type": "Roadm"
-         },
+        },
 
- 	{
+        {
             "allowed": true,
             "latitude": 33.9,
             "longitude": 118.4,
@@ -34,10 +34,10 @@
                 "numRegen": 2
             },
             "type": "Roadm"
-         }
+        }
     ],
 
-        "opticalLinks": [
+    "opticalLinks": [
         {
             "allowed": true,
             "nodeDpid1": "00:00:ff:ff:ff:ff:ff:01",
@@ -51,10 +51,38 @@
                 "port2": 30
             },
             "type": "wdmLink"
-         },
-       
-       {
-	"allowed": true,
+        },
+        {
+            "allowed": true,
+            "nodeDpid1": "00:00:ff:ff:ff:ff:ff:03",
+            "nodeDpid2": "00:00:ff:ff:ff:ff:ff:01",
+            "params": {
+                "distKms": 1000,
+                "nodeName1": "ROADM3",
+                "nodeName2": "ROADM1",
+                "numWaves": 80,
+                "port1": 30,
+                "port2": 10
+            },
+            "type": "wdmLink"
+        },
+
+        {
+            "allowed": true,
+            "nodeDpid1": "00:00:ff:ff:ff:ff:ff:02",
+            "nodeDpid2": "00:00:ff:ff:ff:ff:ff:03",
+            "params": {
+                "distKms": 2000,
+                "nodeName1": "ROADM2",
+                "nodeName2": "ROADM3",
+                "numWaves": 80,
+                "port1": 20,
+                "port2": 31
+            },
+            "type": "wdmLink"
+        },
+        {
+            "allowed": true,
             "nodeDpid1": "00:00:ff:ff:ff:ff:ff:03",
             "nodeDpid2": "00:00:ff:ff:ff:ff:ff:02",
             "params": {
@@ -66,10 +94,9 @@
                 "port2": 20
             },
             "type": "wdmLink"
-       },
+        },
 
-   
-      {
+        {
             "allowed": true,
             "nodeDpid1": "00:00:ff:ff:ff:ff:00:01",
             "nodeDpid2": "00:00:ff:ff:ff:ff:ff:01",
@@ -82,8 +109,21 @@
             },
             "type": "pktOptLink"
         },
+        {
+            "allowed": true,
+            "nodeDpid1": "00:00:ff:ff:ff:ff:ff:01",
+            "nodeDpid2": "00:00:ff:ff:ff:ff:00:01",
+            "params": {
+                "nodeName1": "ROADM1",
+                "nodeName2": "ROUTER1",
+                "bandWidth": 100000,
+                "port1": 11,
+                "port2": 10
+            },
+            "type": "pktOptLink"
+        },
 
-       {
+        {
             "allowed": true,
             "nodeDpid1": "00:00:ff:ff:ff:ff:00:02",
             "nodeDpid2": "00:00:ff:ff:ff:ff:ff:02",
@@ -95,7 +135,20 @@
                 "port2": 21
             },
             "type": "pktOptLink"
-        } 
+        },
+        {
+            "allowed": true,
+            "nodeDpid1": "00:00:ff:ff:ff:ff:ff:02",
+            "nodeDpid2": "00:00:ff:ff:ff:ff:00:02",
+            "params": {
+                "nodeName1": "ROADM2",
+                "nodeName2": "ROUTER2",
+                "bandWidth": 100000,
+                "port1": 21,
+                "port2": 10
+            },
+            "type": "pktOptLink"
+        }
 
     ]
 }
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
index ca25a76..ed5b8df 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -1,14 +1,20 @@
 package org.onlab.onos.sdnip;
 
-import com.google.common.base.Objects;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.collect.SetMultimap;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.googlecode.concurrenttrees.common.KeyValuePair;
-import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
-import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
-import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+
 import org.apache.commons.lang3.tuple.Pair;
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
@@ -36,20 +42,15 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.Semaphore;
+import com.google.common.base.Objects;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.googlecode.concurrenttrees.common.KeyValuePair;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
 
 /**
  * This class processes BGP route update, translates each update into a intent
@@ -744,6 +745,21 @@
     }
 
     /**
+     * Gets the pushed route intents.
+     *
+     * @return the pushed route intents
+     */
+    public Collection<MultiPointToSinglePointIntent> getPushedRouteIntents() {
+        List<MultiPointToSinglePointIntent> pushedIntents = new LinkedList<>();
+
+        for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
+            pushedRouteIntents.entrySet()) {
+            pushedIntents.add(entry.getValue());
+        }
+        return pushedIntents;
+    }
+
+    /**
      * Listener for host events.
      */
     class InternalHostListener implements HostListener {
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
new file mode 100644
index 0000000..9ab5916
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
@@ -0,0 +1,465 @@
+package org.onlab.onos.sdnip;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.ApplicationId;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultHost;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.HostLocation;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.sdnip.config.BgpPeer;
+import org.onlab.onos.sdnip.config.Interface;
+import org.onlab.onos.sdnip.config.SdnIpConfigService;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.TestUtils;
+import org.onlab.util.TestUtils.TestUtilsException;
+
+import com.google.common.collect.Sets;
+
+/**
+ * This class tests adding a route, updating a route, deleting a route,
+ * and adding a route whose next hop is the local BGP speaker.
+ */
+public class RouterTest {
+
+    private SdnIpConfigService sdnIpConfigService;
+    private InterfaceService interfaceService;
+    private IntentService intentService;
+    private HostService hostService;
+
+    private Map<IpAddress, BgpPeer> bgpPeers;
+    private Map<IpAddress, BgpPeer> configuredPeers;
+    private Set<Interface> interfaces;
+    private Set<Interface> configuredInterfaces;
+
+    private static final ApplicationId APPID = new ApplicationId() {
+        @Override
+        public short id() {
+            return 1;
+        }
+
+        @Override
+        public String name() {
+            return "SDNIP";
+        }
+    };
+
+    private Router router;
+
+    @Before
+    public void setUp() throws Exception {
+        bgpPeers = setUpBgpPeers();
+        interfaces = setUpInterfaces();
+        initRouter();
+    }
+
+    /**
+     * Initializes Router class.
+     */
+    private void initRouter() {
+
+        intentService = createMock(IntentService.class);
+        hostService = createMock(HostService.class);
+
+        interfaceService = createMock(InterfaceService.class);
+        expect(interfaceService.getInterfaces()).andReturn(
+                interfaces).anyTimes();
+
+        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
+        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
+        Interface expectedInterface =
+                new Interface(new ConnectPoint(
+                        DeviceId.deviceId("of:0000000000000001"),
+                        PortNumber.portNumber("1")),
+                        ipAddressesOnSw1Eth1,
+                        MacAddress.valueOf("00:00:00:00:00:01"));
+        ConnectPoint egressPoint = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000001"),
+                PortNumber.portNumber(1));
+        expect(interfaceService.getInterface(egressPoint)).andReturn(
+                expectedInterface).anyTimes();
+
+        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
+        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
+        Interface expectedInterfaceNew =
+                new Interface(new ConnectPoint(
+                        DeviceId.deviceId("of:0000000000000002"),
+                        PortNumber.portNumber("1")),
+                        ipAddressesOnSw2Eth1,
+                        MacAddress.valueOf("00:00:00:00:00:02"));
+        ConnectPoint egressPointNew = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000002"),
+                PortNumber.portNumber(1));
+        expect(interfaceService.getInterface(egressPointNew)).andReturn(
+                expectedInterfaceNew).anyTimes();
+        replay(interfaceService);
+
+        sdnIpConfigService = createMock(SdnIpConfigService.class);
+        expect(sdnIpConfigService.getBgpPeers()).andReturn(bgpPeers).anyTimes();
+        replay(sdnIpConfigService);
+
+        router = new Router(APPID, intentService,
+                hostService, sdnIpConfigService, interfaceService);
+    }
+
+    /**
+     * Sets up BGP peers in external networks.
+     *
+     * @return configured BGP peers as a Map from peer IP address to BgpPeer
+     */
+    private Map<IpAddress, BgpPeer> setUpBgpPeers() {
+
+        configuredPeers = new HashMap<>();
+
+        String peerSw1Eth1 = "192.168.10.1";
+        configuredPeers.put(IpAddress.valueOf(peerSw1Eth1),
+                new BgpPeer("00:00:00:00:00:00:00:01", 1, peerSw1Eth1));
+
+        // Two BGP peers are connected to switch 2 port 1.
+        String peer1Sw2Eth1 = "192.168.20.1";
+        configuredPeers.put(IpAddress.valueOf(peer1Sw2Eth1),
+                new BgpPeer("00:00:00:00:00:00:00:02", 1, peer1Sw2Eth1));
+
+        String peer2Sw2Eth1 = "192.168.20.2";
+        configuredPeers.put(IpAddress.valueOf(peer2Sw2Eth1),
+                new BgpPeer("00:00:00:00:00:00:00:02", 1, peer2Sw2Eth1));
+
+        return configuredPeers;
+    }
+
+    /**
+     * Sets up logical interfaces, which emulate the configured interfaces
+     * in SDN-IP application.
+     *
+     * @return configured interfaces as a Set
+     */
+    private Set<Interface> setUpInterfaces() {
+
+        configuredInterfaces = Sets.newHashSet();
+
+        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
+        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
+        configuredInterfaces.add(
+                new Interface(new ConnectPoint(
+                        DeviceId.deviceId("of:0000000000000001"),
+                        PortNumber.portNumber(1)),
+                        ipAddressesOnSw1Eth1,
+                        MacAddress.valueOf("00:00:00:00:00:01")));
+
+        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
+        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
+        configuredInterfaces.add(
+                new Interface(new ConnectPoint(
+                        DeviceId.deviceId("of:0000000000000002"),
+                        PortNumber.portNumber(1)),
+                        ipAddressesOnSw2Eth1,
+                        MacAddress.valueOf("00:00:00:00:00:02")));
+
+        Set<IpPrefix> ipAddressesOnSw3Eth1 = new HashSet<IpPrefix>();
+        ipAddressesOnSw3Eth1.add(IpPrefix.valueOf("192.168.30.0/24"));
+        configuredInterfaces.add(
+                new Interface(new ConnectPoint(
+                        DeviceId.deviceId("of:0000000000000003"),
+                        PortNumber.portNumber(1)),
+                        ipAddressesOnSw3Eth1,
+                        MacAddress.valueOf("00:00:00:00:00:03")));
+
+        return configuredInterfaces;
+    }
+
+    /**
+     * This method tests adding a route entry.
+     */
+    @Test
+    public void testProcessRouteAdd() throws TestUtilsException {
+
+        // Construct a route entry
+        RouteEntry routeEntry = new RouteEntry(
+                IpPrefix.valueOf("1.1.1.0/24"),
+                IpAddress.valueOf("192.168.10.1"));
+
+        // Construct a MultiPointToSinglePointIntent intent
+        TrafficSelector.Builder selectorBuilder =
+                DefaultTrafficSelector.builder();
+        selectorBuilder.matchEthType(Ethernet.TYPE_IPV4).matchIPDst(
+                routeEntry.prefix());
+
+        TrafficTreatment.Builder treatmentBuilder =
+                DefaultTrafficTreatment.builder();
+        treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
+
+        Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000002"),
+                PortNumber.portNumber("1")));
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000003"),
+                PortNumber.portNumber("1")));
+
+        ConnectPoint egressPoint = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000001"),
+                PortNumber.portNumber("1"));
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(APPID,
+                        selectorBuilder.build(), treatmentBuilder.build(),
+                        ingressPoints, egressPoint);
+
+        // Reset host service
+        reset(hostService);
+        Set<Host> hosts = new HashSet<Host>(1);
+        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
+        ipPrefixes.add(IpPrefix.valueOf("192.168.10.1/32"));
+        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:01"), VlanId.NONE,
+                new HostLocation(
+                        DeviceId.deviceId("of:0000000000000001"),
+                        PortNumber.portNumber(1), 1),
+                        ipPrefixes));
+        expect(hostService.getHostsByIp(
+                IpPrefix.valueOf("192.168.10.1/32"))).andReturn(hosts);
+        replay(hostService);
+
+        // Set up test expectation
+        reset(intentService);
+        intentService.submit(intent);
+        replay(intentService);
+
+        // Call the processRouteAdd() method in Router class
+        router.leaderChanged(true);
+        TestUtils.setField(router, "isActivatedLeader", true);
+        router.processRouteAdd(routeEntry);
+
+        // Verify
+        assertEquals(router.getRoutes().size(), 1);
+        assertTrue(router.getRoutes().contains(routeEntry));
+        assertEquals(router.getPushedRouteIntents().size(), 1);
+        assertEquals(router.getPushedRouteIntents().iterator().next(),
+                intent);
+        verify(intentService);
+    }
+
+    /**
+     * This method tests updating a route entry.
+     *
+     * @throws TestUtilsException
+     */
+    @Test
+    public void testRouteUpdate() throws TestUtilsException {
+
+        // Firstly add a route
+        testProcessRouteAdd();
+
+        // Construct the existing route entry
+        RouteEntry routeEntry = new RouteEntry(
+                IpPrefix.valueOf("1.1.1.0/24"),
+                IpAddress.valueOf("192.168.10.1"));
+
+        // Construct the existing MultiPointToSinglePointIntent intent
+        TrafficSelector.Builder selectorBuilder =
+                DefaultTrafficSelector.builder();
+        selectorBuilder.matchEthType(Ethernet.TYPE_IPV4).matchIPDst(
+                routeEntry.prefix());
+
+        TrafficTreatment.Builder treatmentBuilder =
+                DefaultTrafficTreatment.builder();
+        treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
+
+        ConnectPoint egressPoint = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000001"),
+                PortNumber.portNumber("1"));
+
+        Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000002"),
+                PortNumber.portNumber("1")));
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000003"),
+                PortNumber.portNumber("1")));
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(APPID,
+                        selectorBuilder.build(), treatmentBuilder.build(),
+                        ingressPoints, egressPoint);
+
+        // Start to construct a new route entry and new intent
+        RouteEntry routeEntryUpdate = new RouteEntry(
+                IpPrefix.valueOf("1.1.1.0/24"),
+                IpAddress.valueOf("192.168.20.1"));
+
+        // Construct a new MultiPointToSinglePointIntent intent
+        TrafficSelector.Builder selectorBuilderNew =
+                DefaultTrafficSelector.builder();
+        selectorBuilderNew.matchEthType(Ethernet.TYPE_IPV4).matchIPDst(
+                routeEntryUpdate.prefix());
+
+        TrafficTreatment.Builder treatmentBuilderNew =
+                DefaultTrafficTreatment.builder();
+        treatmentBuilderNew.setEthDst(MacAddress.valueOf("00:00:00:00:00:02"));
+
+        ConnectPoint egressPointNew = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000002"),
+                PortNumber.portNumber("1"));
+
+        Set<ConnectPoint> ingressPointsNew = new HashSet<ConnectPoint>();
+        ingressPointsNew.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000001"),
+                PortNumber.portNumber("1")));
+        ingressPointsNew.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000003"),
+                PortNumber.portNumber("1")));
+
+        MultiPointToSinglePointIntent intentNew =
+                new MultiPointToSinglePointIntent(APPID,
+                        selectorBuilderNew.build(),
+                        treatmentBuilderNew.build(),
+                        ingressPointsNew, egressPointNew);
+
+        // Reset host service
+        reset(hostService);
+        Set<Host> hosts = new HashSet<Host>(1);
+        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
+        ipPrefixes.add(IpPrefix.valueOf("192.168.20.1/32"));
+        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:02"), VlanId.NONE,
+                new HostLocation(
+                        DeviceId.deviceId("of:0000000000000002"),
+                        PortNumber.portNumber(1), 1),
+                        ipPrefixes));
+        expect(hostService.getHostsByIp(
+                IpPrefix.valueOf("192.168.20.1/32"))).andReturn(hosts);
+        replay(hostService);
+
+        // Set up test expectation
+        reset(intentService);
+        intentService.withdraw(intent);
+        intentService.submit(intentNew);
+        replay(intentService);
+
+        // Call the processRouteAdd() method in Router class
+        router.leaderChanged(true);
+        TestUtils.setField(router, "isActivatedLeader", true);
+        router.processRouteAdd(routeEntryUpdate);
+
+        // Verify
+        assertEquals(router.getRoutes().size(), 1);
+        assertTrue(router.getRoutes().contains(routeEntryUpdate));
+        assertEquals(router.getPushedRouteIntents().size(), 1);
+        assertEquals(router.getPushedRouteIntents().iterator().next(),
+                intentNew);
+        verify(intentService);
+    }
+
+    /**
+     * This method tests deleting a route entry.
+     */
+    @Test
+    public void testProcessRouteDelete() throws TestUtilsException {
+
+        // Firstly add a route
+        testProcessRouteAdd();
+
+        // Construct the existing route entry
+        RouteEntry routeEntry = new RouteEntry(
+                IpPrefix.valueOf("1.1.1.0/24"),
+                IpAddress.valueOf("192.168.10.1"));
+
+        // Construct the existing MultiPointToSinglePointIntent intent
+        TrafficSelector.Builder selectorBuilder =
+                DefaultTrafficSelector.builder();
+        selectorBuilder.matchEthType(Ethernet.TYPE_IPV4).matchIPDst(
+                routeEntry.prefix());
+
+        TrafficTreatment.Builder treatmentBuilder =
+                DefaultTrafficTreatment.builder();
+        treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
+
+        ConnectPoint egressPoint = new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000001"),
+                PortNumber.portNumber("1"));
+
+        Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000002"),
+                PortNumber.portNumber("1")));
+        ingressPoints.add(new ConnectPoint(
+                DeviceId.deviceId("of:0000000000000003"),
+                PortNumber.portNumber("1")));
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(APPID,
+                        selectorBuilder.build(), treatmentBuilder.build(),
+                        ingressPoints, egressPoint);
+
+        // Set up expectation
+        reset(intentService);
+        intentService.withdraw(intent);
+        replay(intentService);
+
+        // Call route deleting method in Router class
+        router.leaderChanged(true);
+        TestUtils.setField(router, "isActivatedLeader", true);
+        router.processRouteDelete(routeEntry);
+
+        // Verify
+        assertEquals(router.getRoutes().size(), 0);
+        assertEquals(router.getPushedRouteIntents().size(), 0);
+        verify(intentService);
+    }
+
+    /**
+     * This method tests when the next hop of a route is the local BGP speaker.
+     *
+     * @throws TestUtilsException
+     */
+    @Test
+    public void testLocalRouteAdd() throws TestUtilsException {
+
+        // Construct a route entry, the next hop is the local BGP speaker
+        RouteEntry routeEntry = new RouteEntry(
+                IpPrefix.valueOf("1.1.1.0/24"), IpAddress.valueOf("0.0.0.0"));
+
+        // Reset intentService to check whether the submit method is called
+        reset(intentService);
+        replay(intentService);
+
+        // Call the processRouteAdd() method in Router class
+        router.leaderChanged(true);
+        TestUtils.setField(router, "isActivatedLeader", true);
+        router.processRouteAdd(routeEntry);
+
+        // Verify
+        assertEquals(router.getRoutes().size(), 1);
+        assertTrue(router.getRoutes().contains(routeEntry));
+        assertEquals(router.getPushedRouteIntents().size(), 0);
+        verify(intentService);
+    }
+}
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
index ce3dc59..903b352 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
@@ -20,8 +20,10 @@
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.net.topology.Topology;
+import org.onlab.onos.net.topology.TopologyProvider;
 import org.onlab.onos.net.topology.TopologyService;
 
 /**
@@ -35,6 +37,10 @@
     private static final String FMT =
             "time=%s, devices=%d, links=%d, clusters=%d, paths=%d";
 
+    @Option(name = "-r", aliases = "--recompute", description = "Trigger topology re-computation",
+            required = false, multiValued = false)
+    private boolean recompute = false;
+
     protected TopologyService service;
     protected Topology topology;
 
@@ -49,7 +55,10 @@
     @Override
     protected void execute() {
         init();
-        if (outputJson()) {
+        if (recompute) {
+            get(TopologyProvider.class).triggerRecompute();
+
+        } else if (outputJson()) {
             print("%s", new ObjectMapper().createObjectNode()
                     .put("time", topology.time())
                     .put("deviceCount", topology.deviceCount())
diff --git a/core/api/src/main/java/org/onlab/onos/net/Link.java b/core/api/src/main/java/org/onlab/onos/net/Link.java
index 1ae5b9d..3e23dc1 100644
--- a/core/api/src/main/java/org/onlab/onos/net/Link.java
+++ b/core/api/src/main/java/org/onlab/onos/net/Link.java
@@ -25,7 +25,18 @@
         /**
          * Signifies that this link is an edge, i.e. host link.
          */
-        EDGE
+        EDGE,
+
+        /**
+         * Signifies that this link represents a logical link backed by
+         * some form of a tunnel.
+         */
+        TUNNEL,
+
+        /**
+         * Signifies that this link is realized by optical connection.
+         */
+        OPTICAL
     }
 
     /**
@@ -49,6 +60,4 @@
      */
     Type type();
 
-    // LinkInfo info(); // Additional link information / decorations
-
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java
index 7ad8cc0..312e154 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java
@@ -7,4 +7,9 @@
  */
 public interface TopologyProvider extends Provider {
 
+    /**
+     * Triggers topology recomputation.
+     */
+    void triggerRecompute();
+
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/link/impl/LinkManager.java b/core/net/src/main/java/org/onlab/onos/net/link/impl/LinkManager.java
index 835c47e..e59eb9f 100644
--- a/core/net/src/main/java/org/onlab/onos/net/link/impl/LinkManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/link/impl/LinkManager.java
@@ -208,7 +208,7 @@
             LinkEvent event = store.createOrUpdateLink(provider().id(),
                                                        linkDescription);
             if (event != null) {
-                log.debug("Link {} detected", linkDescription);
+                log.info("Link {} detected", linkDescription);
                 post(event);
             }
         }
diff --git a/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java b/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
index 9631c66..0efd08b 100644
--- a/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
+++ b/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
@@ -5,6 +5,7 @@
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
 import org.onlab.onos.event.AbstractEventAccumulator;
 import org.onlab.onos.event.Event;
 import org.onlab.onos.event.EventAccumulator;
@@ -39,6 +40,7 @@
  * new topology snapshots.
  */
 @Component(immediate = true)
+@Service
 public class DefaultTopologyProvider extends AbstractProvider
         implements TopologyProvider {
 
@@ -89,7 +91,7 @@
         linkService.addListener(linkListener);
 
         isStarted = true;
-        triggerTopologyBuild(Collections.<Event>emptyList());
+        triggerRecompute();
         log.info("Started");
     }
 
@@ -108,6 +110,11 @@
         log.info("Stopped");
     }
 
+    @Override
+    public void triggerRecompute() {
+        triggerTopologyBuild(Collections.<Event>emptyList());
+    }
+
     /**
      * Triggers assembly of topology data citing the specified events as the
      * reason.
@@ -177,7 +184,11 @@
 
         @Override
         public void run() {
-            buildTopology(reasons);
+            try {
+                buildTopology(reasons);
+            } catch (Exception e) {
+                log.warn("Unable to compute topology due to: {}", e.getMessage());
+            }
         }
     }
 
diff --git a/core/net/src/test/java/org/onlab/onos/net/topology/impl/TopologyManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/topology/impl/TopologyManagerTest.java
index d369073..ada7e78 100644
--- a/core/net/src/test/java/org/onlab/onos/net/topology/impl/TopologyManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/topology/impl/TopologyManagerTest.java
@@ -195,6 +195,10 @@
         public TestProvider() {
             super(PID);
         }
+
+        @Override
+        public void triggerRecompute() {
+        }
     }
 
     private static class TestListener implements TopologyListener {
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
index 5c87921..09d6a62 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
@@ -192,14 +192,6 @@
     // Creates and stores the link and returns the appropriate event.
     // Guarded by linkDescs value (=locking each Link)
     private LinkEvent createLink(LinkKey key, Link newLink) {
-
-        if (newLink.providerId().isAncillary()) {
-            // TODO: revisit ancillary only Link handling
-
-            // currently treating ancillary only as down (not visible outside)
-            return null;
-        }
-
         links.put(key, newLink);
         srcLinks.put(newLink.src().deviceId(), key);
         dstLinks.put(newLink.dst().deviceId(), key);
@@ -209,10 +201,8 @@
     // Updates, if necessary the specified link and returns the appropriate event.
     // Guarded by linkDescs value (=locking each Link)
     private LinkEvent updateLink(LinkKey key, Link oldLink, Link newLink) {
-
         if (newLink.providerId().isAncillary()) {
             // TODO: revisit ancillary only Link handling
-
             // currently treating ancillary only as down (not visible outside)
             return null;
         }
diff --git a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
index 02cb411..a7f40ac 100644
--- a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
+++ b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
@@ -1,18 +1,6 @@
 package org.onlab.onos.store.trivial.impl;
 
-import static org.junit.Assert.*;
-import static org.onlab.onos.net.DeviceId.deviceId;
-import static org.onlab.onos.net.Link.Type.*;
-import static org.onlab.onos.net.link.LinkEvent.Type.*;
-import static org.onlab.onos.store.trivial.impl.SimpleDeviceStoreTest.assertAnnotationsEquals;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
+import com.google.common.collect.Iterables;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -23,17 +11,27 @@
 import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Link.Type;
 import org.onlab.onos.net.LinkKey;
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.SparseAnnotations;
-import org.onlab.onos.net.Link.Type;
 import org.onlab.onos.net.link.DefaultLinkDescription;
 import org.onlab.onos.net.link.LinkEvent;
 import org.onlab.onos.net.link.LinkStore;
 import org.onlab.onos.net.link.LinkStoreDelegate;
 import org.onlab.onos.net.provider.ProviderId;
 
-import com.google.common.collect.Iterables;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Link.Type.*;
+import static org.onlab.onos.net.link.LinkEvent.Type.*;
+import static org.onlab.onos.store.trivial.impl.SimpleDeviceStoreTest.assertAnnotationsEquals;
 
 /**
  * Test of the simple LinkStore implementation.
@@ -301,7 +299,7 @@
         LinkEvent event = linkStore.createOrUpdateLink(PIDA,
                     new DefaultLinkDescription(src, dst, INDIRECT, A1));
 
-        assertNull("Ancillary only link is ignored", event);
+        assertNotNull("Ancillary only link is ignored", event);
 
         // add Primary link
         LinkEvent event2 = linkStore.createOrUpdateLink(PID,
@@ -309,7 +307,7 @@
 
         assertLink(DID1, P1, DID2, P2, INDIRECT, event2.subject());
         assertAnnotationsEquals(event2.subject().annotations(), A2, A1);
-        assertEquals(LINK_ADDED, event2.type());
+        assertEquals(LINK_UPDATED, event2.type());
 
         // update link type
         LinkEvent event3 = linkStore.createOrUpdateLink(PID,
@@ -375,7 +373,7 @@
     }
 
     @Test
-    public final void testAncillaryOnlyNotVisible() {
+    public final void testAncillaryVisible() {
         ConnectPoint src = new ConnectPoint(DID1, P1);
         ConnectPoint dst = new ConnectPoint(DID2, P2);
 
@@ -384,18 +382,8 @@
                     new DefaultLinkDescription(src, dst, INDIRECT, A1));
 
         // Ancillary only link should not be visible
-        assertEquals(0, linkStore.getLinkCount());
-
-        assertTrue(Iterables.isEmpty(linkStore.getLinks()));
-
-        assertNull(linkStore.getLink(src, dst));
-
-        assertEquals(Collections.emptySet(), linkStore.getIngressLinks(dst));
-
-        assertEquals(Collections.emptySet(), linkStore.getEgressLinks(src));
-
-        assertEquals(Collections.emptySet(), linkStore.getDeviceEgressLinks(DID1));
-        assertEquals(Collections.emptySet(), linkStore.getDeviceIngressLinks(DID2));
+        assertEquals(1, linkStore.getLinkCount());
+        assertNotNull(linkStore.getLink(src, dst));
     }
 
     // If Delegates should be called only on remote events,
diff --git a/features/features.xml b/features/features.xml
index e6c4beb..e363094 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -199,9 +199,16 @@
 
     <feature name="onos-app-metrics" version="1.0.0"
              description="ONOS metrics applications">
+        <feature>onos-app-metrics-intent</feature>
         <feature>onos-app-metrics-topology</feature>
     </feature>
 
+    <feature name="onos-app-metrics-intent" version="1.0.0"
+             description="ONOS intent metrics application">
+        <feature>onos-api</feature>
+        <bundle>mvn:org.onlab.onos/onos-app-metrics-intent/1.0.0-SNAPSHOT</bundle>
+    </feature>
+
     <feature name="onos-app-metrics-topology" version="1.0.0"
              description="ONOS topology metrics application">
         <feature>onos-api</feature>
diff --git a/pom.xml b/pom.xml
index cef3b79..8aa5ce4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,7 +88,6 @@
                 <version>18.0</version>
             </dependency>
 
-
             <dependency>
                 <groupId>io.netty</groupId>
                 <artifactId>netty</artifactId>
diff --git a/tools/test/cells/single b/tools/test/cells/single
index 6b13756..125477a 100644
--- a/tools/test/cells/single
+++ b/tools/test/cells/single
@@ -7,4 +7,4 @@
 export OCN="192.168.56.103"
 export OCI="${OC1}"
 
-export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core-trivial,onos-cli,onos-openflow,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
+export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core-trivial,onos-cli,onos-rest,onos-openflow,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
diff --git a/tools/test/topos/oe-linear-3.json b/tools/test/topos/oe-linear-3.json
new file mode 100644
index 0000000..9214bd9
--- /dev/null
+++ b/tools/test/topos/oe-linear-3.json
@@ -0,0 +1,45 @@
+{
+    "devices" : [
+        {
+            "uri": "of:0000ffffffffff01", "mac": "ffffffffffff01", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?",
+            "annotations": { "latitude": 37.6, "longitude": 122.3, "optical.regens": 0 }
+        },
+        {
+            "uri": "of:0000ffffffffff02", "mac": "ffffffffffff02", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?",
+            "annotations": { "latitude": 37.3, "longitude": 121.9, "optical.regens": 0 }
+        },
+        {
+            "uri": "of:0000ffffffffff03", "mac": "ffffffffffff03", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?",
+            "annotations": { "latitude": 33.9, "longitude": 118.4, "optical.regens": 2 }
+        },
+
+        {
+            "uri": "of:0000ffffffff0001", "mac": "ffffffffff0003", "type": "SWITCH",
+            "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?",
+            "annotations": { "latitude": 37.6, "longitude": 122.3 }
+        },
+        {
+            "uri": "of:0000ffffffff0002", "mac": "ffffffffff0002", "type": "SWITCH",
+            "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?",
+            "annotations": { "latitude": 37.3, "longitude": 121.9 }
+        }
+    ],
+
+    "links" : [
+        { "src": "of:0000ffffffffff01/10", "dst": "of:0000ffffffffff03/30", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM" } },
+        { "src": "of:0000ffffffffff02/20", "dst": "of:0000ffffffffff03/31", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM" } },
+
+        { "src": "of:0000ffffffff0001/10", "dst": "of:0000ffffffffff01/11", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect" } },
+        { "src": "of:0000ffffffff0002/10", "dst": "of:0000ffffffffff02/21", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect" } }
+    ],
+
+    "hosts" : [
+        { "mac": "a0:00:00:00:00:11", "vlan": -1, "location": "of:0000ffffffff0001/11", "ip": "1.2.3.4" },
+        { "mac": "a0:00:00:00:00:12", "vlan": -1, "location": "of:0000ffffffff0001/12", "ip": "1.2.3.5" },
+        { "mac": "a0:00:00:00:00:21", "vlan": -1, "location": "of:0000ffffffff0002/11", "ip": "2.2.3.4" },
+        { "mac": "a0:00:00:00:00:22", "vlan": -1, "location": "of:0000ffffffff0002/12", "ip": "2.2.3.5" }
+    ]
+}
\ No newline at end of file
diff --git a/utils/misc/src/main/java/org/onlab/packet/ChassisId.java b/utils/misc/src/main/java/org/onlab/packet/ChassisId.java
index 3029647..5b48e63 100644
--- a/utils/misc/src/main/java/org/onlab/packet/ChassisId.java
+++ b/utils/misc/src/main/java/org/onlab/packet/ChassisId.java
@@ -32,7 +32,7 @@
      * @param value the value to use.
      */
     public ChassisId(String value) {
-        this.value = Long.valueOf(value);
+        this.value = Long.valueOf(value, 16);
     }
 
     /**
diff --git a/utils/nio/src/test/java/org/onlab/nio/IOLoopIntegrationTest.java b/utils/nio/src/test/java/org/onlab/nio/IOLoopIntegrationTest.java
index e61f6e2..55a0130 100644
--- a/utils/nio/src/test/java/org/onlab/nio/IOLoopIntegrationTest.java
+++ b/utils/nio/src/test/java/org/onlab/nio/IOLoopIntegrationTest.java
@@ -1,6 +1,7 @@
 package org.onlab.nio;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.net.InetAddress;
@@ -33,7 +34,8 @@
         }
     }
 
-
+    // TODO: this test can not pass in some environments, need to be improved
+    @Ignore
     @Test
     public void basic() throws Exception {
         runTest(MILLION, MESSAGE_LENGTH, TIMEOUT);
diff --git a/web/api/pom.xml b/web/api/pom.xml
index e6f9ff8..da8fd1c 100644
--- a/web/api/pom.xml
+++ b/web/api/pom.xml
@@ -23,12 +23,6 @@
             <version>1.0.0-SNAPSHOT</version>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <scope>test</scope>
-        </dependency>
-
     </dependencies>
 
     <properties>
diff --git a/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java b/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java
new file mode 100644
index 0000000..3120511
--- /dev/null
+++ b/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.onlab.onos.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultAnnotations;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.HostLocation;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.SparseAnnotations;
+import org.onlab.onos.net.device.DefaultDeviceDescription;
+import org.onlab.onos.net.device.DeviceDescription;
+import org.onlab.onos.net.device.DeviceProvider;
+import org.onlab.onos.net.device.DeviceProviderRegistry;
+import org.onlab.onos.net.device.DeviceProviderService;
+import org.onlab.onos.net.host.DefaultHostDescription;
+import org.onlab.onos.net.host.HostProvider;
+import org.onlab.onos.net.host.HostProviderRegistry;
+import org.onlab.onos.net.host.HostProviderService;
+import org.onlab.onos.net.link.DefaultLinkDescription;
+import org.onlab.onos.net.link.LinkProvider;
+import org.onlab.onos.net.link.LinkProviderRegistry;
+import org.onlab.onos.net.link.LinkProviderService;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+
+import java.net.URI;
+import java.util.Iterator;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Provider of devices and links parsed from a JSON configuration structure.
+ */
+class ConfigProvider implements DeviceProvider, LinkProvider, HostProvider {
+
+    private static final ProviderId PID =
+            new ProviderId("cfg", "org.onlab.onos.rest", true);
+
+    private final JsonNode cfg;
+    private final DeviceProviderRegistry deviceProviderRegistry;
+    private final LinkProviderRegistry linkProviderRegistry;
+    private final HostProviderRegistry hostProviderRegistry;
+
+    /**
+     * Creates a new configuration provider.
+     *
+     * @param cfg                    JSON configuration
+     * @param deviceProviderRegistry device provider registry
+     * @param linkProviderRegistry   link provider registry
+     * @param hostProviderRegistry   host provider registry
+     */
+    ConfigProvider(JsonNode cfg,
+                   DeviceProviderRegistry deviceProviderRegistry,
+                   LinkProviderRegistry linkProviderRegistry,
+                   HostProviderRegistry hostProviderRegistry) {
+        this.cfg = checkNotNull(cfg, "Configuration cannot be null");
+        this.deviceProviderRegistry = checkNotNull(deviceProviderRegistry, "Device provider registry cannot be null");
+        this.linkProviderRegistry = checkNotNull(linkProviderRegistry, "Link provider registry cannot be null");
+        this.hostProviderRegistry = checkNotNull(hostProviderRegistry, "Host provider registry cannot be null");
+    }
+
+    /**
+     * Parses the given JSON and provides links as configured.
+     */
+    void parse() {
+        parseDevices();
+        parseLinks();
+        parseHosts();
+    }
+
+    // Parses the given JSON and provides devices.
+    private void parseDevices() {
+        try {
+            DeviceProviderService dps = deviceProviderRegistry.register(this);
+            JsonNode nodes = cfg.get("devices");
+            if (nodes != null) {
+                for (JsonNode node : nodes) {
+                    parseDevice(dps, node);
+                }
+            }
+        } finally {
+            deviceProviderRegistry.unregister(this);
+        }
+    }
+
+    // Parses the given node with device data and supplies the device.
+    private void parseDevice(DeviceProviderService dps, JsonNode node) {
+        URI uri = URI.create(get(node, "uri"));
+        Device.Type type = Device.Type.valueOf(get(node, "type"));
+        String mfr = get(node, "mfr");
+        String hw = get(node, "hw");
+        String sw = get(node, "sw");
+        String serial = get(node, "serial");
+        ChassisId cid = new ChassisId(get(node, "mac"));
+        SparseAnnotations annotations = annotations(node.get("annotations"));
+
+        DeviceDescription desc =
+                new DefaultDeviceDescription(uri, type, mfr, hw, sw, serial,
+                                             cid, annotations);
+        dps.deviceConnected(deviceId(uri), desc);
+    }
+
+    // Parses the given JSON and provides links as configured.
+    private void parseLinks() {
+        try {
+            LinkProviderService lps = linkProviderRegistry.register(this);
+            JsonNode nodes = cfg.get("links");
+            if (nodes != null) {
+                for (JsonNode node : nodes) {
+                    parseLink(lps, node, false);
+                    if (!node.has("halfplex")) {
+                        parseLink(lps, node, true);
+                    }
+                }
+            }
+        } finally {
+            linkProviderRegistry.unregister(this);
+        }
+    }
+
+    // Parses the given node with link data and supplies the link.
+    private void parseLink(LinkProviderService lps, JsonNode node, boolean reverse) {
+        ConnectPoint src = connectPoint(get(node, "src"));
+        ConnectPoint dst = connectPoint(get(node, "dst"));
+        Link.Type type = Link.Type.valueOf(get(node, "type"));
+        SparseAnnotations annotations = annotations(node.get("annotations"));
+
+        DefaultLinkDescription desc = reverse ?
+                new DefaultLinkDescription(dst, src, type, annotations) :
+                new DefaultLinkDescription(src, dst, type, annotations);
+        lps.linkDetected(desc);
+    }
+
+    // Parses the given JSON and provides hosts as configured.
+    private void parseHosts() {
+        try {
+            HostProviderService hps = hostProviderRegistry.register(this);
+            JsonNode nodes = cfg.get("hosts");
+            if (nodes != null) {
+                for (JsonNode node : nodes) {
+                    parseHost(hps, node);
+                }
+            }
+        } finally {
+            hostProviderRegistry.unregister(this);
+        }
+    }
+
+    // Parses the given node with host data and supplies the host.
+    private void parseHost(HostProviderService hps, JsonNode node) {
+        MacAddress mac = MacAddress.valueOf(get(node, "mac"));
+        VlanId vlanId = VlanId.vlanId(node.get("vlan").shortValue());
+        HostId hostId = HostId.hostId(mac, vlanId);
+        SparseAnnotations annotations = annotations(node.get("annotations"));
+        HostLocation location = new HostLocation(connectPoint(get(node, "location")), 0);
+        IpPrefix ip = IpPrefix.valueOf(get(node, "ip"));
+
+        DefaultHostDescription desc =
+                new DefaultHostDescription(mac, vlanId, location, ip, annotations);
+        hps.hostDetected(hostId, desc);
+    }
+
+    // Produces set of annotations from the given JSON node.
+    private SparseAnnotations annotations(JsonNode node) {
+        if (node == null) {
+            return null;
+        }
+
+        DefaultAnnotations.Builder builder = DefaultAnnotations.builder();
+        Iterator<String> it = node.fieldNames();
+        while (it.hasNext()) {
+            String k = it.next();
+            builder.set(k, node.get(k).asText());
+        }
+        return builder.build();
+    }
+
+    // Produces a connection point from the specified uri/port text.
+    private ConnectPoint connectPoint(String text) {
+        int i = text.lastIndexOf("/");
+        return new ConnectPoint(deviceId(text.substring(0, i)),
+                                portNumber(text.substring(i + 1)));
+    }
+
+    // Returns string form of the named property in the given JSON object.
+    private String get(JsonNode node, String name) {
+        return node.path(name).asText();
+    }
+
+    @Override
+    public void triggerProbe(Device device) {
+    }
+
+    @Override
+    public void roleChanged(Device device, MastershipRole newRole) {
+    }
+
+    @Override
+    public void triggerProbe(Host host) {
+    }
+
+    @Override
+    public ProviderId id() {
+        return PID;
+    }
+}
diff --git a/web/api/src/main/java/org/onlab/onos/rest/ConfigResource.java b/web/api/src/main/java/org/onlab/onos/rest/ConfigResource.java
new file mode 100644
index 0000000..219abbd
--- /dev/null
+++ b/web/api/src/main/java/org/onlab/onos/rest/ConfigResource.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.onlab.onos.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.onlab.onos.net.device.DeviceProviderRegistry;
+import org.onlab.onos.net.host.HostProviderRegistry;
+import org.onlab.onos.net.link.LinkProviderRegistry;
+import org.onlab.rest.BaseResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Resource that acts as an ancillary provider for uploading pre-configured
+ * devices, ports and links.
+ */
+@Path("config")
+public class ConfigResource extends BaseResource {
+
+    @POST
+    @Path("topology")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response topology(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode cfg = mapper.readTree(input);
+        new ConfigProvider(cfg, get(DeviceProviderRegistry.class),
+                           get(LinkProviderRegistry.class),
+                           get(HostProviderRegistry.class)).parse();
+        return Response.ok(mapper.createObjectNode().toString()).build();
+    }
+
+}
diff --git a/web/api/src/test/java/org/onlab/onos/rest/topo.json b/web/api/src/test/java/org/onlab/onos/rest/topo.json
new file mode 100644
index 0000000..cdef976
--- /dev/null
+++ b/web/api/src/test/java/org/onlab/onos/rest/topo.json
@@ -0,0 +1,19 @@
+{
+    "devices" : [
+        {
+            "uri": "of:00000000000001", "type": "ROADM", "mfr": "Foo, Inc.", "hw": "Alpha", "sw": "1.2.3",
+            "serial": "ab321", "mac": "00000000000001", "annotations": {"foo": "bar"},
+            "ports": []
+        },
+        {
+            "uri": "of:00000000000002", "type": "ROADM", "mfr": "Foo, Inc.", "hw": "Alpha", "sw": "1.2.3",
+            "serial": "ab456", "mac": "00000000000002", "annotations": {"foo": "bar"},
+            "ports": []
+        }
+    ],
+
+    "links" : [
+        { "src": "of:00000000000001/1", "dst": "of:00000000000002/1", "type": "OPTICAL" },
+        { "src": "of:00000000000002/1", "dst": "of:00000000000001/1", "type": "OPTICAL" }
+    ]
+}
\ No newline at end of file
diff --git a/web/pom.xml b/web/pom.xml
index 3e9f2a0..ebe4d89 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -44,6 +44,11 @@
         </dependency>
 
         <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>com.sun.jersey</groupId>
             <artifactId>jersey-servlet</artifactId>
         </dependency>
@@ -93,6 +98,7 @@
                             ${project.groupId}.${project.artifactId}
                         </Bundle-SymbolicName>
                         <Import-Package>
+                            org.slf4j,
                             org.osgi.framework,
                             javax.ws.rs,javax.ws.rs.core,
                             com.sun.jersey.api.core,
@@ -100,6 +106,8 @@
                             com.sun.jersey.server.impl.container.servlet,
                             com.fasterxml.jackson.databind,
                             com.fasterxml.jackson.databind.node,
+                            com.google.common.base.*,
+                            org.onlab.packet.*,
                             org.onlab.rest.*,
                             org.onlab.onos.*
                         </Import-Package>