Resolving merge conflicts
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 86c1c0b..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
@@ -83,7 +83,7 @@
     protected OpticalNetworkConfig opticalNetworkConfig;
 
     public OpticalConfigProvider() {
-        super(new ProviderId("of", "org.onlab.onos.provider.opticalConfig", true));
+        super(new ProviderId("optical", "org.onlab.onos.provider.opticalConfig", true));
     }
 
     @Activate
@@ -238,7 +238,7 @@
         while (iterWdmNode.hasNext()) {
             Roadm value = iterWdmNode.next();
             DeviceId did = deviceId("of:" + value.getNodeId().replace(":", ""));
-            ChassisId cid = new ChassisId(value.getNodeId());
+            ChassisId cid = new ChassisId();
             DefaultAnnotations extendedAttributes = DefaultAnnotations.builder()
                     .set(OPTICAL_ANNOTATION + "switchType", "ROADM")
                     .set(OPTICAL_ANNOTATION + "switchName", value.getName())
@@ -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/HostToInterfaceAdaptor.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
index 7a6e6bb..c609b95 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
@@ -4,19 +4,17 @@
 
 import java.util.Set;
 
-import org.apache.commons.lang.NotImplementedException;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.host.PortAddresses;
 import org.onlab.onos.sdnip.config.Interface;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
 
 import com.google.common.collect.Sets;
 
-
-
 /**
- * Provides IntefaceService using PortAddresses data from the HostService.
+ * Provides InterfaceService using PortAddresses data from the HostService.
  */
 public class HostToInterfaceAdaptor implements InterfaceService {
 
@@ -52,8 +50,17 @@
 
     @Override
     public Interface getMatchingInterface(IpAddress ipAddress) {
-        // TODO implement
-        throw new NotImplementedException("getMatchingInteface is not yet implemented");
+        checkNotNull(ipAddress);
+
+        for (PortAddresses portAddresses : hostService.getAddressBindings()) {
+            for (IpPrefix p : portAddresses.ips()) {
+                if (p.contains(ipAddress)) {
+                    return new Interface(portAddresses);
+                }
+            }
+        }
+
+        return null;
     }
 
 }
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/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index 54aa9e5..d66ec9c 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -64,6 +64,7 @@
         bgpSessionManager.startUp(2000); // TODO
 
         // TODO need to disable link discovery on external ports
+
     }
 
     @Deactivate
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java
new file mode 100644
index 0000000..5a3dc51
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java
@@ -0,0 +1,177 @@
+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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.host.PortAddresses;
+import org.onlab.onos.sdnip.config.Interface;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+/**
+ * Unit tests for the HostToInterfaceAdaptor class.
+ */
+public class HostToInterfaceAdaptorTest {
+
+    private HostService hostService;
+    private HostToInterfaceAdaptor adaptor;
+
+    private Set<PortAddresses> portAddresses;
+    private Map<ConnectPoint, Interface> interfaces;
+
+    private static final ConnectPoint CP1 = new ConnectPoint(
+            DeviceId.deviceId("of:1"), PortNumber.portNumber(1));
+    private static final ConnectPoint CP2 = new ConnectPoint(
+            DeviceId.deviceId("of:1"), PortNumber.portNumber(2));
+    private static final ConnectPoint CP3 = new ConnectPoint(
+            DeviceId.deviceId("of:2"), PortNumber.portNumber(1));
+
+    private static final ConnectPoint NON_EXISTENT_CP = new ConnectPoint(
+            DeviceId.deviceId("doesnotexist"), PortNumber.portNumber(1));
+
+    private static final PortAddresses DEFAULT_PA = new PortAddresses(
+            NON_EXISTENT_CP, null, null);
+
+
+    @Before
+    public void setUp() throws Exception {
+        hostService = createMock(HostService.class);
+
+        portAddresses = Sets.newHashSet();
+        interfaces = Maps.newHashMap();
+
+        createPortAddressesAndInterface(CP1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.1.1/24")),
+                MacAddress.valueOf("00:00:00:00:00:01"));
+
+        // Two addresses in the same subnet
+        createPortAddressesAndInterface(CP2,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.2.1/24"),
+                        IpPrefix.valueOf("192.168.2.2/24")),
+                MacAddress.valueOf("00:00:00:00:00:02"));
+
+        // Two addresses in different subnets
+        createPortAddressesAndInterface(CP3,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.3.1/24"),
+                        IpPrefix.valueOf("192.168.4.1/24")),
+                MacAddress.valueOf("00:00:00:00:00:03"));
+
+        expect(hostService.getAddressBindings()).andReturn(portAddresses).anyTimes();
+
+        replay(hostService);
+
+        adaptor = new HostToInterfaceAdaptor(hostService);
+    }
+
+    /**
+     * Creates both a PortAddresses and an Interface for the given inputs and
+     * places them in the correct global data stores.
+     *
+     * @param cp the connect point
+     * @param ips the set of IP addresses
+     * @param mac the MAC address
+     */
+    private void createPortAddressesAndInterface(
+            ConnectPoint cp, Set<IpPrefix> ips, MacAddress mac) {
+        PortAddresses pa = new PortAddresses(cp, ips, mac);
+        portAddresses.add(pa);
+        expect(hostService.getAddressBindingsForPort(cp)).andReturn(pa).anyTimes();
+
+        Interface intf = new Interface(cp, ips, mac);
+        interfaces.put(cp, intf);
+    }
+
+    /**
+     * Tests {@link HostToInterfaceAdaptor#getInterfaces()}.
+     * Verifies that the set of interfaces returned matches what is expected
+     * based on the input PortAddresses data.
+     */
+    @Test
+    public void testGetInterfaces() {
+        Set<Interface> adaptorIntfs = adaptor.getInterfaces();
+
+        assertEquals(3, adaptorIntfs.size());
+        assertTrue(adaptorIntfs.contains(this.interfaces.get(CP1)));
+        assertTrue(adaptorIntfs.contains(this.interfaces.get(CP2)));
+        assertTrue(adaptorIntfs.contains(this.interfaces.get(CP3)));
+    }
+
+    /**
+     * Tests {@link HostToInterfaceAdaptor#getInterface(ConnectPoint)}.
+     * Verifies that the correct interface is returned for a given connect
+     * point.
+     */
+    @Test
+    public void testGetInterface() {
+        assertEquals(this.interfaces.get(CP1), adaptor.getInterface(CP1));
+        assertEquals(this.interfaces.get(CP2), adaptor.getInterface(CP2));
+        assertEquals(this.interfaces.get(CP3), adaptor.getInterface(CP3));
+
+        // Try and get an interface for a connect point with no addresses
+        reset(hostService);
+        expect(hostService.getAddressBindingsForPort(NON_EXISTENT_CP))
+                .andReturn(DEFAULT_PA).anyTimes();
+        replay(hostService);
+
+        assertNull(adaptor.getInterface(NON_EXISTENT_CP));
+    }
+
+    /**
+     * Tests {@link HostToInterfaceAdaptor#getInterface(ConnectPoint)} in the
+     * case that the input connect point is null.
+     * Verifies that a NullPointerException is thrown.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testGetInterfaceNull() {
+        adaptor.getInterface(null);
+    }
+
+    /**
+     * Tests {@link HostToInterfaceAdaptor#getMatchingInterface(IpAddress)}.
+     * Verifies that the correct interface is returned based on the given IP
+     * address.
+     */
+    @Test
+    public void testGetMatchingInterface() {
+        assertEquals(this.interfaces.get(CP1),
+                adaptor.getMatchingInterface(IpAddress.valueOf("192.168.1.100")));
+        assertEquals(this.interfaces.get(CP2),
+                adaptor.getMatchingInterface(IpAddress.valueOf("192.168.2.100")));
+        assertEquals(this.interfaces.get(CP3),
+                adaptor.getMatchingInterface(IpAddress.valueOf("192.168.3.100")));
+        assertEquals(this.interfaces.get(CP3),
+                adaptor.getMatchingInterface(IpAddress.valueOf("192.168.4.100")));
+
+        // Try and match an address we don't have subnet configured for
+        assertNull(adaptor.getMatchingInterface(IpAddress.valueOf("1.1.1.1")));
+    }
+
+    /**
+     * Tests {@link HostToInterfaceAdaptor#getMatchingInterface(IpAddress)} in the
+     * case that the input IP address is null.
+     * Verifies that a NullPointerException is thrown.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testGetMatchingInterfaceNull() {
+        adaptor.getMatchingInterface(null);
+    }
+
+}
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/AbstractShellCommand.java b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
index 628754a..68b3244 100644
--- a/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
@@ -18,10 +18,13 @@
  */
 package org.onlab.onos.cli;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Option;
 import org.apache.karaf.shell.console.OsgiCommandSupport;
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.CoreService;
+import org.onlab.onos.net.Annotations;
 import org.onlab.osgi.DefaultServiceDirectory;
 import org.onlab.osgi.ServiceNotFoundException;
 
@@ -76,6 +79,34 @@
     }
 
     /**
+     * Produces a string image of the specified key/value annotations.
+     *
+     * @param annotations key/value annotations
+     * @return string image with ", k1=v1, k2=v2, ..." pairs
+     */
+    public static String annotations(Annotations annotations) {
+        StringBuilder sb = new StringBuilder();
+        for (String key : annotations.keys()) {
+            sb.append(", ").append(key).append('=').append(annotations.value(key));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Produces a JSON object from the specified key/value annotations.
+     *
+     * @param annotations key/value annotations
+     * @return JSON object
+     */
+    public static ObjectNode annotations(ObjectMapper mapper, Annotations annotations) {
+        ObjectNode result = mapper.createObjectNode();
+        for (String key : annotations.keys()) {
+            result.put(key, annotations.value(key));
+        }
+        return result;
+    }
+
+    /**
      * Executes this command.
      */
     protected abstract void execute();
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
index f2c280e..3be8e5c 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
@@ -43,7 +43,7 @@
          description = "Lists all ports or all ports of a device")
 public class DevicePortsListCommand extends DevicesListCommand {
 
-    private static final String FMT = "  port=%s, state=%s";
+    private static final String FMT = "  port=%s, state=%s%s";
 
     @Option(name = "-e", aliases = "--enabled", description = "Show only enabled ports",
             required = false, multiValued = false)
@@ -112,7 +112,8 @@
             if (isIncluded(port)) {
                 ports.add(mapper.createObjectNode()
                                   .put("port", port.number().toString())
-                                  .put("isEnabled", port.isEnabled()));
+                                  .put("isEnabled", port.isEnabled())
+                                  .set("annotations", annotations(mapper, port.annotations())));
             }
         }
         return result.put("device", device.id().toString()).set("ports", ports);
@@ -131,7 +132,8 @@
         Collections.sort(ports, Comparators.PORT_COMPARATOR);
         for (Port port : ports) {
             if (isIncluded(port)) {
-                print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled");
+                print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled",
+                      annotations(port.annotations()));
             }
         }
     }
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
index 095e2c0..543954e 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
@@ -41,7 +41,7 @@
 public class DevicesListCommand extends AbstractShellCommand {
 
     private static final String FMT =
-            "id=%s, available=%s, role=%s, type=%s, mfr=%s, hw=%s, sw=%s, serial=%s";
+            "id=%s, available=%s, role=%s, type=%s, mfr=%s, hw=%s, sw=%s, serial=%s%s";
 
     @Override
     protected void execute() {
@@ -89,7 +89,8 @@
                     .put("mfr", device.manufacturer())
                     .put("hw", device.hwVersion())
                     .put("sw", device.swVersion())
-                    .put("serial", device.serialNumber());
+                    .put("serial", device.serialNumber())
+                    .set("annotations", annotations(mapper, device.annotations()));
         }
         return result;
     }
@@ -117,7 +118,7 @@
             print(FMT, device.id(), service.isAvailable(device.id()),
                   service.getRole(device.id()), device.type(),
                   device.manufacturer(), device.hwVersion(), device.swVersion(),
-                  device.serialNumber());
+                  device.serialNumber(), annotations(device.annotations()));
         }
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/GetStatistics.java b/cli/src/main/java/org/onlab/onos/cli/net/GetStatistics.java
new file mode 100644
index 0000000..3745fa9
--- /dev/null
+++ b/cli/src/main/java/org/onlab/onos/cli/net/GetStatistics.java
@@ -0,0 +1,88 @@
+/*
+ * 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.cli.net;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.PortNumber;
+
+import org.onlab.onos.net.statistic.Load;
+import org.onlab.onos.net.statistic.StatisticService;
+
+
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Fetches statistics.
+ */
+@Command(scope = "onos", name = "get-stats",
+         description = "Fetches stats for a connection point")
+public class GetStatistics extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "connectPoint",
+              description = "Device/Port Description",
+              required = true, multiValued = false)
+    String connectPoint = null;
+
+
+    @Override
+    protected void execute() {
+        StatisticService service = get(StatisticService.class);
+
+        DeviceId ingressDeviceId = deviceId(getDeviceId(connectPoint));
+        PortNumber ingressPortNumber = portNumber(getPortNumber(connectPoint));
+        ConnectPoint cp = new ConnectPoint(ingressDeviceId, ingressPortNumber);
+
+        Load load = service.load(cp);
+
+        print("Load on %s -> %s", cp, load);
+    }
+
+    /**
+     * Extracts the port number portion of the ConnectPoint.
+     *
+     * @param deviceString string representing the device/port
+     * @return port number as a string, empty string if the port is not found
+     */
+    private String getPortNumber(String deviceString) {
+        int slash = deviceString.indexOf('/');
+        if (slash <= 0) {
+            return "";
+        }
+        return deviceString.substring(slash + 1, deviceString.length());
+    }
+
+    /**
+     * Extracts the device ID portion of the ConnectPoint.
+     *
+     * @param deviceString string representing the device/port
+     * @return device ID string
+     */
+    private String getDeviceId(String deviceString) {
+        int slash = deviceString.indexOf('/');
+        if (slash <= 0) {
+            return "";
+        }
+        return deviceString.substring(0, slash);
+    }
+}
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
index 2291f4e..0265116 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
@@ -42,7 +42,7 @@
 public class HostsListCommand extends AbstractShellCommand {
 
     private static final String FMT =
-            "id=%s, mac=%s, location=%s/%s, vlan=%s, ip(s)=%s";
+            "id=%s, mac=%s, location=%s/%s, vlan=%s, ip(s)=%s%s";
 
     @Override
     protected void execute() {
@@ -80,6 +80,7 @@
                 .put("vlan", host.vlan().toString());
         result.set("location", loc);
         result.set("ips", ips);
+        result.set("annotations", annotations(mapper, host.annotations()));
         return result;
     }
 
@@ -105,7 +106,8 @@
             print(FMT, host.id(), host.mac(),
                   host.location().deviceId(),
                   host.location().port(),
-                  host.vlan(), host.ipAddresses());
+                  host.vlan(), host.ipAddresses(),
+                  annotations(host.annotations()));
         }
     }
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
index e6867f6..7889dcf 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
@@ -38,7 +38,7 @@
          description = "Lists all infrastructure links")
 public class LinksListCommand extends AbstractShellCommand {
 
-    private static final String FMT = "src=%s/%s, dst=%s/%s, type=%s";
+    private static final String FMT = "src=%s/%s, dst=%s/%s, type=%s%s";
     private static final String COMPACT = "%s/%s-%s/%s";
 
     @Argument(index = 0, name = "uri", description = "Device ID",
@@ -85,6 +85,7 @@
         ObjectNode result = mapper.createObjectNode();
         result.set("src", json(mapper, link.src()));
         result.set("dst", json(mapper, link.dst()));
+        result.set("annotations", annotations(mapper, link.annotations()));
         return result;
     }
 
@@ -109,7 +110,8 @@
      */
     public static String linkString(Link link) {
         return String.format(FMT, link.src().deviceId(), link.src().port(),
-                             link.dst().deviceId(), link.dst().port(), link.type());
+                             link.dst().deviceId(), link.dst().port(), link.type(),
+                             annotations(link.annotations()));
     }
 
     /**
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/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 9018eb1..b01d412 100644
--- a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -119,6 +119,12 @@
             </optional-completers>
         </command>
         <command>
+            <action class="org.onlab.onos.cli.net.GetStatistics"/>
+                <completers>
+                    <ref component-id="connectPointCompleter"/>
+                </completers>
+        </command>
+        <command>
             <action class="org.onlab.onos.cli.net.AddMultiPointToSinglePointIntentCommand"/>
             <completers>
                 <ref component-id="connectPointCompleter"/>
diff --git a/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java b/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
index dcb2d95..fdbb645 100644
--- a/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
@@ -1,6 +1,5 @@
 package org.onlab.onos.mastership;
 
-import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.cluster.RoleInfo;
 import org.onlab.onos.event.AbstractEvent;
 import org.onlab.onos.net.DeviceId;
@@ -56,19 +55,6 @@
     }
 
     /**
-     * Returns the NodeID of the node associated with the event.
-     * For MASTER_CHANGED this is the newly elected master, and for
-     * BACKUPS_CHANGED, this is the node that was newly added, removed, or
-     * whose position was changed in the list.
-     *
-     * @return node ID as a subject
-     */
-    //XXX to-be removed - or keep for convenience?
-    public NodeId node() {
-        return roleInfo.master();
-    }
-
-    /**
      * Returns the current role state for the subject.
      *
      * @return RoleInfo associated with Device ID subject
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/device/DeviceEvent.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java
index 0a33777..07bd426 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java
@@ -4,6 +4,8 @@
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Port;
 
+import static com.google.common.base.MoreObjects.toStringHelper;
+
 /**
  * Describes infrastructure device event.
  */
@@ -109,4 +111,12 @@
         return port;
     }
 
+    @Override
+    public String toString() {
+        if (port == null) {
+            return super.toString();
+        }
+        return toStringHelper(this).add("time", time()).add("type", type())
+            .add("subject", subject()).add("port", port).toString();
+     }
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java
index 9934b8d..ec73ce5 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java
@@ -13,7 +13,7 @@
 
     /**
      * Triggers an asynchronous probe of the specified device, intended to
-     * determine whether the host is present or not. An indirect result of this
+     * determine whether the device is present or not. An indirect result of this
      * should be invocation of
      * {@link org.onlab.onos.net.device.DeviceProviderService#deviceConnected} )} or
      * {@link org.onlab.onos.net.device.DeviceProviderService#deviceDisconnected}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperation.java
similarity index 74%
rename from core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
rename to core/api/src/main/java/org/onlab/onos/net/flow/BatchOperation.java
index 72a9847..38f0211 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperation.java
@@ -1,5 +1,22 @@
-package org.onlab.onos.net.intent;
-//TODO is this the right package?
+/*
+ * 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.net.flow;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationEntry.java
similarity index 67%
rename from core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
rename to core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationEntry.java
index 4e57d33..2ea7053 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationEntry.java
@@ -1,5 +1,22 @@
-package org.onlab.onos.net.intent;
-//TODO is this the right package?
+/*
+ * 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.net.flow;
 
 import java.util.Objects;
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationResult.java b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationResult.java
index 33f1845..d2db96b 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationResult.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationResult.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.Set;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationTarget.java b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationTarget.java
new file mode 100644
index 0000000..e73e3a8
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/BatchOperationTarget.java
@@ -0,0 +1,26 @@
+/*
+ * 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.net.flow;
+
+/**
+ * An interface of the class which is assigned to BatchOperation.
+ */
+public interface BatchOperationTarget {
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/CompletedBatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/flow/CompletedBatchOperation.java
index 4e671e3..363831c 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/CompletedBatchOperation.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/CompletedBatchOperation.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.Set;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowEntry.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowEntry.java
index cf448cb..59aed23 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowEntry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowEntry.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
index a6593a8..53a94bb 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
index f175bd3..abb29a6 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.HashMap;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
index 5e64c64..b4d8c3e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.LinkedList;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowEntry.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowEntry.java
index cdccaa9..1e6ce52 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowEntry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowEntry.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowId.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowId.java
index 52eac0a..142e352 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowId.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import com.google.common.base.Objects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRule.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRule.java
index 3ac277d..718edfe 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRule.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRule.java
@@ -1,7 +1,24 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.intent.BatchOperationTarget;
 
 /**
  * Represents a generalized match &amp; action pair to be applied to
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEntry.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEntry.java
index d5a1472..ee85ea8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEntry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEntry.java
@@ -1,7 +1,24 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.net.flow.FlowRuleBatchEntry.FlowRuleOperation;
-import org.onlab.onos.net.intent.BatchOperationEntry;
 
 
 public class FlowRuleBatchEntry
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchOperation.java
index 74ef165..f078fb9 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchOperation.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchOperation.java
@@ -1,9 +1,25 @@
+/*
+ * 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.net.flow;
 
 import java.util.Collection;
 
-import org.onlab.onos.net.intent.BatchOperation;
-
 public class FlowRuleBatchOperation
     extends BatchOperation<FlowRuleBatchEntry> {
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleEvent.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleEvent.java
index 97efa5a..e511be6 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleEvent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.event.AbstractEvent;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleListener.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleListener.java
index 114732b..eddaf05 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleListener.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleListener.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.event.EventListener;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
index 5a57b88..ae74ac5 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
@@ -1,7 +1,24 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.ApplicationId;
-import org.onlab.onos.net.intent.BatchOperation;
 import org.onlab.onos.net.provider.Provider;
 
 import com.google.common.util.concurrent.ListenableFuture;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java
index 099d9f4..25b00c4 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.net.provider.ProviderRegistry;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
index 8164579..bacff0d 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.net.DeviceId;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
index 6d04810..0b6f6c7 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.concurrent.Future;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
index c53a32d..11bd4ad 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.concurrent.Future;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStoreDelegate.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStoreDelegate.java
index 66973dd..fbd6b55 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStoreDelegate.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStoreDelegate.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.store.StoreDelegate;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/StoredFlowEntry.java b/core/api/src/main/java/org/onlab/onos/net/flow/StoredFlowEntry.java
index e68ed68..9ba53e9 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/StoredFlowEntry.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/StoredFlowEntry.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
index 41bceb8..b4d566c 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.Set;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficTreatment.java b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficTreatment.java
index fe21328..a576138 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficTreatment.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficTreatment.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import java.util.List;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/Treatment.java b/core/api/src/main/java/org/onlab/onos/net/flow/Treatment.java
index 8318b66..877da65 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/Treatment.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/Treatment.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow;
 
 import org.onlab.onos.net.PortNumber;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
index ebd672c..fb5fb97 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.criteria;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criterion.java b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criterion.java
index 567e100..5337852 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criterion.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criterion.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.criteria;
 
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/package-info.java b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/package-info.java
index 82eb210..fabf82e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/package-info.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/package-info.java
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
 /**
  * Traffic selection criteria model.
  */
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instruction.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instruction.java
index f05d238..084ffe4 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instruction.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instruction.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.instructions;
 
 /**
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instructions.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instructions.java
index b2ebdee..988c52f 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instructions.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/Instructions.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.instructions;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L2ModificationInstruction.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L2ModificationInstruction.java
index 2152532..35acd16 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L2ModificationInstruction.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L2ModificationInstruction.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.instructions;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L3ModificationInstruction.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L3ModificationInstruction.java
index 7ec8c84..8aff5df 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L3ModificationInstruction.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L3ModificationInstruction.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.flow.instructions;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/package-info.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/package-info.java
index 01b68ad..067cd01 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/package-info.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/package-info.java
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
 /**
  * Traffic treatment model.
  */
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/package-info.java b/core/api/src/main/java/org/onlab/onos/net/flow/package-info.java
index 76bee78..8fd6008 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/package-info.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/package-info.java
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
 /**
  * Flow rule model &amp; related services API definitions.
  */
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
deleted file mode 100644
index c678f31..0000000
--- a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.onlab.onos.net.intent;
-//TODO is this the right package?
-
-/**
- * An interface of the class which is assigned to BatchOperation.
- */
-public interface BatchOperationTarget {
-
-}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
index 8d634a4..710ae4e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.collect.ImmutableSet;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
index 2d42f58..795f681 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
index 4c11f57..b5fefb2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
@@ -1,7 +1,26 @@
+/*
+ * 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.net.intent;
 
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.NetworkResource;
+import org.onlab.onos.net.flow.BatchOperationTarget;
 
 import java.util.Collection;
 import java.util.Objects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
deleted file mode 100644
index b450d71..0000000
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.onlab.onos.net.intent;
-
-/**
- * A list of intent operations.
- */
-public class IntentBatchOperation extends
-        BatchOperation<BatchOperationEntry<IntentBatchOperation.Operator, ?>> {
-    /**
-     * The intent operators.
-     */
-    public enum Operator {
-        ADD,
-        REMOVE,
-    }
-
-    /**
-     * Adds an add-intent operation.
-     *
-     * @param intent the intent to be added
-     * @return the IntentBatchOperation object if succeeded, null otherwise
-     */
-    public IntentBatchOperation addAddIntentOperation(Intent intent) {
-        return (null == super.addOperation(
-                new BatchOperationEntry<Operator, Intent>(Operator.ADD, intent)))
-                ? null : this;
-    }
-
-    /**
-     * Adds a remove-intent operation.
-     *
-     * @param id the ID of intent to be removed
-     * @return the IntentBatchOperation object if succeeded, null otherwise
-     */
-    public IntentBatchOperation addRemoveIntentOperation(IntentId id) {
-        return (null == super.addOperation(
-                new BatchOperationEntry<Operator, IntentId>(Operator.REMOVE, id)))
-                ? null : this;
-    }
-}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
index dbc3cc4..c54b601 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import java.util.List;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
index 742a590..22a24f9 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import org.onlab.onos.event.AbstractEvent;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
index fff55be..55ca32e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 /**
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
index 50e803c..31db2ec 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import java.util.Map;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
index 08ab7fa..8cfe6fd 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
@@ -1,5 +1,25 @@
+/*
+ * 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.net.intent;
 
+import org.onlab.onos.net.flow.BatchOperationTarget;
+
 /**
  * Intent identifier suitable as an external key.
  * <p/>
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
index c403846..22669e5 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import java.util.List;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentListener.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentListener.java
index c00c1f6..aa1a4a0 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentListener.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentListener.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import org.onlab.onos.event.EventListener;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperation.java
new file mode 100644
index 0000000..4135ed8
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperation.java
@@ -0,0 +1,91 @@
+/*
+ * 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.net.intent;
+
+/**
+ * Abstraction of an intent-related operation, e.g. add, remove, replace.
+ */
+public class IntentOperation {
+
+    private final Type type;
+    private final IntentId intentId;
+    private final Intent intent;
+
+    /**
+     * Operation type.
+     */
+    enum Type {
+        /**
+         * Indicates that an intent should be added.
+         */
+        SUBMIT,
+
+        /**
+         * Indicates that an intent should be removed.
+         */
+        WITHDRAW,
+
+        /**
+         * Indicates that an intent should be replaced with another.
+         */
+        REPLACE
+    }
+
+    /**
+     * Creates an intent operation.
+     *
+     * @param type operation type
+     * @param intentId identifier of the intent subject to the operation
+     * @param intent intent subject
+     */
+    IntentOperation(Type type, IntentId intentId, Intent intent) {
+        this.type = type;
+        this.intentId = intentId;
+        this.intent = intent;
+    }
+
+    /**
+     * Returns the type of the operation.
+     *
+     * @return operation type
+     */
+    public Type type() {
+        return type;
+    }
+
+    /**
+     * Returns the identifier of the intent to which this operation applies.
+     *
+     * @return intent identifier
+     */
+    public IntentId intentId() {
+        return intentId;
+    }
+
+    /**
+     * Returns the intent to which this operation applied. For remove,
+     * this can be null.
+     *
+     * @return intent that is the subject of the operation; null for remove
+     */
+    public Intent intent() {
+        return intent;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
index 470d98b..32f0414 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
@@ -1,10 +1,123 @@
+/*
+ * 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.net.intent;
 
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.onos.net.intent.IntentOperation.Type.REPLACE;
+import static org.onlab.onos.net.intent.IntentOperation.Type.SUBMIT;
+import static org.onlab.onos.net.intent.IntentOperation.Type.WITHDRAW;
+
 /**
- * Abstraction of a batch of intent submit/withdraw operations.
+ * Batch of intent submit/withdraw/replace operations.
  */
-public interface IntentOperations {
+public final class IntentOperations {
 
-    // TODO: elaborate once the revised BatchOperation scheme is in place
+    private final List<IntentOperation> operations;
 
+    /**
+     * Creates a batch of intent operations using the supplied list.
+     *
+     * @param operations list of intent operations
+     */
+    private IntentOperations(List<IntentOperation> operations) {
+        this.operations = operations;
+    }
+
+    /**
+     * List of operations that need to be executed as a unit.
+     *
+     * @return list of intent operations
+     */
+    public List<IntentOperation> operations() {
+        return operations;
+    }
+
+    /**
+     * Returns a builder for intent operation batches.
+     *
+     * @return intent operations builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder for batches of intent operations.
+     */
+    public static final class Builder {
+
+        private final ImmutableList.Builder<IntentOperation> builder = ImmutableList.builder();
+
+        // Public construction is forbidden.
+        private Builder() {
+        }
+
+        /**
+         * Adds an intent submit operation.
+         *
+         * @param intent intent to be submitted
+         * @return self
+         */
+        public Builder addSubmitOperation(Intent intent) {
+            checkNotNull(intent, "Intent cannot be null");
+            builder.add(new IntentOperation(SUBMIT, intent.id(), intent));
+            return this;
+        }
+
+        /**
+         * Adds an intent submit operation.
+         *
+         * @param oldIntentId intent to be replaced
+         * @param newIntent   replacement intent
+         * @return self
+         */
+        public Builder addReplaceOperation(IntentId oldIntentId, Intent newIntent) {
+            checkNotNull(oldIntentId, "Intent ID cannot be null");
+            checkNotNull(newIntent, "Intent cannot be null");
+            builder.add(new IntentOperation(REPLACE, oldIntentId, newIntent));
+            return this;
+        }
+
+        /**
+         * Adds an intent submit operation.
+         *
+         * @param intentId identifier of the intent to be withdrawn
+         * @return self
+         */
+        public Builder addWithdrawOperation(IntentId intentId) {
+            checkNotNull(intentId, "Intent ID cannot be null");
+            builder.add(new IntentOperation(WITHDRAW, intentId, null));
+            return this;
+        }
+
+        /**
+         * Builds a batch of intent operations.
+         *
+         * @return immutable batch of intent operations
+         */
+        public IntentOperations build() {
+            return new IntentOperations(builder.build());
+        }
+
+    }
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
index 700066d..6b89a68 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
@@ -1,7 +1,26 @@
+/*
+ * 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.net.intent;
 
 
 import java.util.List;
+import java.util.concurrent.Future;
 
 /**
  * Service for application submitting or withdrawing their intents.
@@ -28,6 +47,14 @@
     void withdraw(Intent intent);
 
     /**
+     * Replaces the specified intent with a new one.
+     *
+     * @param oldIntentId identifier of the old intent being replaced
+     * @param newIntent new intent replacing the old one
+     */
+    void replace(IntentId oldIntentId, Intent newIntent);
+
+    /**
      * Submits a batch of submit &amp; withdraw operations. Such a batch is
      * assumed to be processed together.
      * <p/>
@@ -36,7 +63,7 @@
      *
      * @param operations batch of intent operations
      */
-    void execute(IntentOperations operations);
+    Future<IntentOperations> execute(IntentOperations operations);
 
     /**
      * Returns an iterable of intents currently in the system.
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
index bd140af..38662d2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 /**
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentStore.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentStore.java
index c234b0f..7a62d61 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentStore.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import org.onlab.onos.store.Store;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentStoreDelegate.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentStoreDelegate.java
index 6c37db8..ab117d2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentStoreDelegate.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentStoreDelegate.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import org.onlab.onos.store.StoreDelegate;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
index 1b465aa..65d1ba8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
index 7c638aa..13324d0 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
index 1e41120..d7996c9 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
index d170c27..7b91cdf 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
index 62e36e7..e5790d0 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
@@ -1,3 +1,21 @@
+/*
+ * 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.net.intent;
 
 import com.google.common.base.MoreObjects;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
index 3e5e46f..d7efa52 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
 /**
  * Set of abstractions for conveying high-level intents for treatment of
  * selected network traffic by allowing applications to express the
diff --git a/core/api/src/main/java/org/onlab/onos/net/statistic/DefaultLoad.java b/core/api/src/main/java/org/onlab/onos/net/statistic/DefaultLoad.java
new file mode 100644
index 0000000..f0f890a
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/statistic/DefaultLoad.java
@@ -0,0 +1,64 @@
+package org.onlab.onos.net.statistic;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.onos.net.flow.FlowRuleProvider;
+
+/**
+ * Implementation of a load.
+ */
+public class DefaultLoad implements Load {
+
+    private final boolean isValid;
+    private final long current;
+    private final long previous;
+    private final long time;
+
+    /**
+     * Creates an invalid load.
+     */
+    public DefaultLoad() {
+        this.isValid = false;
+        this.time = System.currentTimeMillis();
+        this.current = -1;
+        this.previous = -1;
+    }
+
+    /**
+     * Creates a load value from the parameters.
+     * @param current the current value
+     * @param previous the previous value
+     */
+    public DefaultLoad(long current, long previous) {
+        this.current = current;
+        this.previous = previous;
+        this.time = System.currentTimeMillis();
+        this.isValid = true;
+    }
+
+    @Override
+    public long rate() {
+        return (current - previous) / FlowRuleProvider.POLL_INTERVAL;
+    }
+
+    @Override
+    public long latest() {
+        return current;
+    }
+
+    @Override
+    public boolean isValid() {
+        return isValid;
+    }
+
+    @Override
+    public long time() {
+        return time;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper("Load").add("rate", rate())
+                .add("latest", latest()).toString();
+
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/statistic/Load.java b/core/api/src/main/java/org/onlab/onos/net/statistic/Load.java
index 534b10c..b609f2b 100644
--- a/core/api/src/main/java/org/onlab/onos/net/statistic/Load.java
+++ b/core/api/src/main/java/org/onlab/onos/net/statistic/Load.java
@@ -6,15 +6,27 @@
 public interface Load {
 
     /**
-     * Obtain the current observed rate on a link.
+     * Obtain the current observed rate (in bytes/s) on a link.
      * @return long value
      */
     long rate();
 
     /**
-     * Obtain the latest counter viewed on that link.
+     * Obtain the latest bytes counter viewed on that link.
      * @return long value
      */
     long latest();
 
+    /**
+     * Indicates whether this load was built on valid values.
+     * @return boolean
+     */
+    boolean isValid();
+
+    /**
+     * Returns when this value was seen.
+     * @return epoch time
+     */
+    long time();
+
 }
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/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessageResponse.java b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessageResponse.java
index ae2089d..d2a0039 100644
--- a/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessageResponse.java
+++ b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessageResponse.java
@@ -1,12 +1,18 @@
 package org.onlab.onos.store.cluster.messaging;
 
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 import org.onlab.onos.cluster.NodeId;
 
-public interface ClusterMessageResponse {
+public interface ClusterMessageResponse extends Future<byte[]> {
+
     public NodeId sender();
-    public byte[] get(long timeout, TimeUnit timeunit) throws TimeoutException;
-    public byte[] get(long timeout) throws InterruptedException;
+
+    // TODO InterruptedException, ExecutionException removed from original
+    // Future declaration. Revisit if we ever need those.
+    @Override
+    public byte[] get(long timeout, TimeUnit unit) throws TimeoutException;
+
 }
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
index 5020459..cb982ba 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
@@ -9,6 +9,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 /**
  * Fake implementation of the intent service to assist in developing tests of
@@ -171,11 +172,17 @@
     }
 
     @Override
-    public void execute(IntentOperations operations) {
+    public void replace(IntentId oldIntentId, Intent newIntent) {
         // TODO: implement later
     }
 
     @Override
+    public Future<IntentOperations> execute(IntentOperations operations) {
+        // TODO: implement later
+        return null;
+    }
+
+    @Override
     public Set<Intent> getIntents() {
         return Collections.unmodifiableSet(new HashSet<>(intents.values()));
     }
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
index b2c172a..1a671f6 100644
--- a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
@@ -227,10 +227,14 @@
             if (clusterService.getNodes().size() > (clusterSize.intValue() / 2)) {
                 return true;
             }
-            //else {
+//            else {
                 //FIXME: break tie for equal-sized clusters, by number of
                 //       connected switches, then masters, then nodeId hash
-            // }
+                //       problem is, how do we get at channel info cleanly here?
+                //       Also, what's the time hit for a distributed store look-up
+                //       versus channel re-negotiation? bet on the latter being worse.
+
+//            }
             return false;
         }
 
diff --git a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
index fd47dc7..a157e50 100644
--- a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
@@ -161,6 +161,17 @@
         }
     }
 
+    // Queries a device for port information.
+    private void queryPortInfo(DeviceId deviceId) {
+        Device device = store.getDevice(deviceId);
+        // FIXME: Device might not be there yet. (eventual consistent)
+        if (device == null) {
+            return;
+        }
+        DeviceProvider provider = getProvider(device.providerId());
+        provider.triggerProbe(device);
+    }
+
     @Override
     public void removeDevice(DeviceId deviceId) {
         checkNotNull(deviceId, DEVICE_ID_NULL);
@@ -210,8 +221,6 @@
             log.info("Device {} connected", deviceId);
             // check my Role
             MastershipRole role = mastershipService.requestRoleFor(deviceId);
-            log.info("## - our role for {} is {} [master is {}]", deviceId, role,
-                    mastershipService.getMasterFor(deviceId));
             if (role != MastershipRole.MASTER) {
                 // TODO: Do we need to explicitly tell the Provider that
                 // this instance is no longer the MASTER? probably not
@@ -265,7 +274,6 @@
             // but if I was the last STANDBY connection, etc. and no one else
             // was there to mark the device offline, this instance may need to
             // temporarily request for Master Role and mark offline.
-            log.info("## for {} role is {}", deviceId, mastershipService.getLocalRole(deviceId));
             if (!mastershipService.getLocalRole(deviceId).equals(MastershipRole.MASTER)) {
                 log.debug("Device {} disconnected, but I am not the master", deviceId);
                 //let go of ability to be backup
@@ -373,7 +381,6 @@
             final DeviceId did = event.subject();
             final NodeId myNodeId = clusterService.getLocalNode().id();
 
-            log.info("## got Mastershipevent for dev {}", did);
             if (myNodeId.equals(event.roleInfo().master())) {
                 MastershipTerm term = termService.getMastershipTerm(did);
 
@@ -384,7 +391,6 @@
                     return;
                 }
 
-                log.info("## setting term for CPS as new master for {}", did);
                 // only set the new term if I am the master
                 deviceClockProviderService.setMastershipTerm(did, term);
 
@@ -404,6 +410,7 @@
                                     device.serialNumber(), device.chassisId()));
                 }
                 //TODO re-collect device information to fix potential staleness
+                queryPortInfo(did);
                 applyRole(did, MastershipRole.MASTER);
             } else if (event.roleInfo().backups().contains(myNodeId)) {
                 applyRole(did, MastershipRole.STANDBY);
diff --git a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
index ba37d22..3ef9fc8 100644
--- a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
@@ -286,7 +286,8 @@
         private void extraneousFlow(FlowRule flowRule) {
             checkNotNull(flowRule, FLOW_RULE_NULL);
             checkValidity();
-            removeFlowRules(flowRule);
+            FlowRuleProvider frp = getProvider(flowRule.deviceId());
+            frp.removeFlowRule(flowRule);
             log.debug("Flow {} is on switch but not in store.", flowRule);
         }
 
@@ -380,14 +381,14 @@
             final FlowRuleBatchRequest request = event.subject();
             switch (event.type()) {
             case BATCH_OPERATION_REQUESTED:
-//                for (FlowEntry entry : request.toAdd()) {
-//                    //eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADD_REQUESTED, entry));
-//                }
-//                for (FlowEntry entry : request.toRemove()) {
-//                    //eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_REMOVE_REQUESTED, entry));
-//                }
-//                // FIXME: what about op.equals(FlowRuleOperation.MODIFY) ?
-//
+                for (FlowEntry entry : request.toAdd()) {
+                    eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADD_REQUESTED, entry));
+                }
+                for (FlowEntry entry : request.toRemove()) {
+                    eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_REMOVE_REQUESTED, entry));
+                }
+                // FIXME: what about op.equals(FlowRuleOperation.MODIFY) ?
+
                 FlowRuleBatchOperation batchOperation = request.asBatchOperation();
 
                 FlowRuleProvider flowRuleProvider =
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/IntentManager.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/IntentManager.java
index bfdc57e..3a3d476 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/IntentManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/IntentManager.java
@@ -126,7 +126,13 @@
 
     // FIXME: implement this method
     @Override
-    public void execute(IntentOperations operations) {
+    public void replace(IntentId oldIntentId, Intent newIntent) {
+        throw new UnsupportedOperationException("execute() is not implemented yet");
+    }
+
+    // FIXME: implement this method
+    @Override
+    public Future<IntentOperations> execute(IntentOperations operations) {
         throw new UnsupportedOperationException("execute() is not implemented yet");
     }
 
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/packet/impl/PacketManager.java b/core/net/src/main/java/org/onlab/onos/net/packet/impl/PacketManager.java
index e682c49..49d21e0 100644
--- a/core/net/src/main/java/org/onlab/onos/net/packet/impl/PacketManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/packet/impl/PacketManager.java
@@ -68,7 +68,9 @@
         checkNotNull(packet, "Packet cannot be null");
         final Device device = deviceService.getDevice(packet.sendThrough());
         final PacketProvider packetProvider = getProvider(device.providerId());
-        packetProvider.emit(packet);
+        if (packetProvider != null) {
+            packetProvider.emit(packet);
+        }
     }
 
     @Override
diff --git a/core/net/src/main/java/org/onlab/onos/net/statistic/impl/StatisticManager.java b/core/net/src/main/java/org/onlab/onos/net/statistic/impl/StatisticManager.java
index 4bca215..6935148 100644
--- a/core/net/src/main/java/org/onlab/onos/net/statistic/impl/StatisticManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/statistic/impl/StatisticManager.java
@@ -9,14 +9,18 @@
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
+
+import org.onlab.onos.net.flow.FlowEntry;
 import org.onlab.onos.net.flow.FlowRule;
 import org.onlab.onos.net.flow.FlowRuleEvent;
 import org.onlab.onos.net.flow.FlowRuleListener;
 import org.onlab.onos.net.flow.FlowRuleService;
+import org.onlab.onos.net.statistic.DefaultLoad;
 import org.onlab.onos.net.statistic.Load;
 import org.onlab.onos.net.statistic.StatisticService;
 import org.onlab.onos.net.statistic.StatisticStore;
 import org.slf4j.Logger;
+import java.util.Set;
 
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -35,12 +39,14 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected StatisticStore statisticStore;
 
+
     private final InternalFlowRuleListener listener = new InternalFlowRuleListener();
 
     @Activate
     public void activate() {
         flowRuleService.addListener(listener);
         log.info("Started");
+
     }
 
     @Deactivate
@@ -51,27 +57,91 @@
 
     @Override
     public Load load(Link link) {
-        return null;
+       return load(link.src());
     }
 
     @Override
     public Load load(ConnectPoint connectPoint) {
-        return null;
+        return loadInternal(connectPoint);
     }
 
     @Override
     public Link max(Path path) {
-        return null;
+        if (path.links().isEmpty()) {
+            return null;
+        }
+        Load maxLoad = new DefaultLoad();
+        Link maxLink = null;
+        for (Link link : path.links()) {
+            Load load = loadInternal(link.src());
+            if (load.rate() > maxLoad.rate()) {
+                maxLoad = load;
+                maxLink = link;
+            }
+        }
+        return maxLink;
     }
 
     @Override
     public Link min(Path path) {
-        return null;
+        if (path.links().isEmpty()) {
+            return null;
+        }
+        Load minLoad = new DefaultLoad();
+        Link minLink = null;
+        for (Link link : path.links()) {
+            Load load = loadInternal(link.src());
+            if (load.rate() < minLoad.rate()) {
+                minLoad = load;
+                minLink = link;
+            }
+        }
+        return minLink;
     }
 
     @Override
     public FlowRule highestHitter(ConnectPoint connectPoint) {
-        return null;
+        Set<FlowEntry> hitters = statisticStore.getCurrentStatistic(connectPoint);
+        if (hitters.isEmpty()) {
+            return null;
+        }
+
+        FlowEntry max = hitters.iterator().next();
+        for (FlowEntry entry : hitters) {
+            if (entry.bytes() > max.bytes()) {
+                max = entry;
+            }
+        }
+        return max;
+    }
+
+    private Load loadInternal(ConnectPoint connectPoint) {
+        Set<FlowEntry> current;
+        Set<FlowEntry> previous;
+        synchronized (statisticStore) {
+            current = statisticStore.getCurrentStatistic(connectPoint);
+            previous = statisticStore.getPreviousStatistic(connectPoint);
+        }
+        if (current == null || previous == null) {
+            return new DefaultLoad();
+        }
+        long currentAggregate = aggregate(current);
+        long previousAggregate = aggregate(previous);
+
+        return new DefaultLoad(currentAggregate, previousAggregate);
+    }
+
+    /**
+     * Aggregates a set of values.
+     * @param values the values to aggregate
+     * @return a long value
+     */
+    private long aggregate(Set<FlowEntry> values) {
+        long sum = 0;
+        for (FlowEntry f : values) {
+            sum += f.bytes();
+        }
+        return sum;
     }
 
     /**
@@ -81,7 +151,29 @@
 
         @Override
         public void event(FlowRuleEvent event) {
-
+            FlowRule rule = event.subject();
+            switch (event.type()) {
+                case RULE_ADDED:
+                case RULE_UPDATED:
+                    if (rule instanceof FlowEntry) {
+                        statisticStore.addOrUpdateStatistic((FlowEntry) rule);
+                    } else {
+                        log.warn("IT AIN'T A FLOWENTRY");
+                    }
+                    break;
+                case RULE_ADD_REQUESTED:
+                    log.info("Preparing for stats");
+                    statisticStore.prepareForStatistics(rule);
+                    break;
+                case RULE_REMOVE_REQUESTED:
+                    log.info("Removing stats");
+                    statisticStore.removeFromStatistics(rule);
+                    break;
+                case RULE_REMOVED:
+                    break;
+                default:
+                    log.warn("Unknown flow rule event {}", 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/event/impl/TestEventDispatcher.java b/core/net/src/test/java/org/onlab/onos/event/impl/TestEventDispatcher.java
index 9eb3980..d2cbc24 100644
--- a/core/net/src/test/java/org/onlab/onos/event/impl/TestEventDispatcher.java
+++ b/core/net/src/test/java/org/onlab/onos/event/impl/TestEventDispatcher.java
@@ -15,6 +15,7 @@
         implements EventDeliveryService {
 
     @Override
+    @SuppressWarnings("unchecked")
     public void post(Event event) {
         EventSink sink = getSink(event.getClass());
         checkState(sink != null, "No sink for event %s", event);
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
index 59b2963..1677af6 100644
--- a/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
@@ -9,6 +9,9 @@
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_UPDATED;
 
+
+import static org.onlab.onos.net.flow.FlowRuleEvent.Type.*;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -55,7 +58,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.flow.criteria.Criterion;
 import org.onlab.onos.net.flow.instructions.Instruction;
-import org.onlab.onos.net.intent.BatchOperation;
+import org.onlab.onos.net.flow.BatchOperation;
 import org.onlab.onos.net.provider.AbstractProvider;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.trivial.impl.SimpleFlowRuleStore;
@@ -165,7 +168,8 @@
         assertEquals("2 rules should exist", 2, flowCount());
 
         providerService.pushFlowMetrics(DID, ImmutableList.of(fe1, fe2));
-        validateEvents(RULE_ADDED, RULE_ADDED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED,
+                       RULE_ADDED, RULE_ADDED);
 
         addFlowRule(1);
         assertEquals("should still be 2 rules", 2, flowCount());
@@ -218,11 +222,12 @@
         FlowEntry fe2 = new DefaultFlowEntry(f2);
         FlowEntry fe3 = new DefaultFlowEntry(f3);
         providerService.pushFlowMetrics(DID, ImmutableList.of(fe1, fe2, fe3));
-        validateEvents(RULE_ADDED, RULE_ADDED, RULE_ADDED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED, RULE_ADD_REQUESTED,
+                       RULE_ADDED, RULE_ADDED, RULE_ADDED);
 
         mgr.removeFlowRules(f1, f2);
         //removing from north, so no events generated
-        validateEvents();
+        validateEvents(RULE_REMOVE_REQUESTED, RULE_REMOVE_REQUESTED);
         assertEquals("3 rule should exist", 3, flowCount());
         assertTrue("Entries should be pending remove.",
                    validateState(ImmutableMap.of(
@@ -244,7 +249,8 @@
         service.removeFlowRules(f1);
         fe1.setState(FlowEntryState.REMOVED);
         providerService.flowRemoved(fe1);
-        validateEvents(RULE_ADDED, RULE_ADDED, RULE_REMOVED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED, RULE_ADDED,
+                       RULE_ADDED, RULE_REMOVE_REQUESTED, RULE_REMOVED);
 
         providerService.flowRemoved(fe1);
         validateEvents();
@@ -253,7 +259,7 @@
         FlowEntry fe3 = new DefaultFlowEntry(f3);
         service.applyFlowRules(f3);
         providerService.pushFlowMetrics(DID, Collections.singletonList(fe3));
-        validateEvents(RULE_ADDED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADDED);
 
         providerService.flowRemoved(fe3);
         validateEvents();
@@ -282,7 +288,8 @@
                         f2, FlowEntryState.ADDED,
                         f3, FlowEntryState.PENDING_ADD)));
 
-        validateEvents(RULE_ADDED, RULE_ADDED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED, RULE_ADD_REQUESTED,
+                       RULE_ADDED, RULE_ADDED);
     }
 
     @Test
@@ -302,7 +309,7 @@
 
         providerService.pushFlowMetrics(DID, Lists.newArrayList(fe1, fe2, fe3));
 
-        validateEvents(RULE_ADDED, RULE_ADDED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED, RULE_ADDED, RULE_ADDED);
 
     }
 
@@ -327,7 +334,8 @@
 
         providerService.pushFlowMetrics(DID, Lists.newArrayList(fe1, fe2));
 
-        validateEvents(RULE_ADDED, RULE_ADDED, RULE_REMOVED);
+        validateEvents(RULE_ADD_REQUESTED, RULE_ADD_REQUESTED, RULE_ADD_REQUESTED,
+                       RULE_REMOVE_REQUESTED, RULE_ADDED, RULE_ADDED, RULE_REMOVED);
 
     }
 
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/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
index 42d89de..4db23ce 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
@@ -4,9 +4,9 @@
 
 import java.io.IOException;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -181,10 +181,13 @@
         }
     }
 
-    private static final class InternalClusterMessageResponse implements ClusterMessageResponse {
+    private static final class InternalClusterMessageResponse
+        implements ClusterMessageResponse {
 
         private final NodeId sender;
         private final Response responseFuture;
+        private volatile boolean isCancelled = false;
+        private volatile boolean isDone = false;
 
         public InternalClusterMessageResponse(NodeId sender, Response responseFuture) {
             this.sender = sender;
@@ -198,12 +201,39 @@
         @Override
         public byte[] get(long timeout, TimeUnit timeunit)
                 throws TimeoutException {
-            return responseFuture.get(timeout, timeunit);
+            final byte[] result = responseFuture.get(timeout, timeunit);
+            isDone = true;
+            return result;
         }
 
         @Override
-        public byte[] get(long timeout) throws InterruptedException {
-            return responseFuture.get();
+        public boolean cancel(boolean mayInterruptIfRunning) {
+            if (isDone()) {
+                return false;
+            }
+            // doing nothing for now
+            // when onlab.netty Response support cancel, call them.
+            isCancelled = true;
+            return true;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return isCancelled;
+        }
+
+        @Override
+        public boolean isDone() {
+            return this.isDone || isCancelled();
+        }
+
+        @Override
+        public byte[] get() throws InterruptedException, ExecutionException {
+            // TODO: consider forbidding this call and force the use of timed get
+            //       to enforce handling of remote peer failure scenario
+            final byte[] result = responseFuture.get();
+            isDone = true;
+            return result;
         }
     }
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
index ac9fc3e..21941b5 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
@@ -290,12 +290,17 @@
     private DeviceEvent updateDevice(ProviderId providerId,
                                      Device oldDevice,
                                      Device newDevice, Timestamp newTimestamp) {
-
         // We allow only certain attributes to trigger update
-        if (!Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
-            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion()) ||
-            !AnnotationsUtil.isEqual(oldDevice.annotations(), newDevice.annotations())) {
+        boolean propertiesChanged =
+                !Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
+                        !Objects.equals(oldDevice.swVersion(), newDevice.swVersion());
+        boolean annotationsChanged =
+                !AnnotationsUtil.isEqual(oldDevice.annotations(), newDevice.annotations());
 
+        // Primary providers can respond to all changes, but ancillary ones
+        // should respond only to annotation changes.
+        if ((providerId.isAncillary() && annotationsChanged) ||
+                (!providerId.isAncillary() && (propertiesChanged || annotationsChanged))) {
             boolean replaced = devices.replace(newDevice.id(), oldDevice, newDevice);
             if (!replaced) {
                 verify(replaced,
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
index 5f97afd..c789c96 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
@@ -35,6 +35,8 @@
                                Class<InternalDeviceEvent> type) {
         ProviderId providerId = (ProviderId) kryo.readClassAndObject(input);
         DeviceId deviceId = (DeviceId) kryo.readClassAndObject(input);
+
+        @SuppressWarnings("unchecked")
         Timestamped<DeviceDescription> deviceDescription
             = (Timestamped<DeviceDescription>) kryo.readClassAndObject(input);
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
index 4f4a9e6..2facb7e 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
@@ -37,6 +37,8 @@
                                Class<InternalPortEvent> type) {
         ProviderId providerId = (ProviderId) kryo.readClassAndObject(input);
         DeviceId deviceId = (DeviceId) kryo.readClassAndObject(input);
+
+        @SuppressWarnings("unchecked")
         Timestamped<List<PortDescription>> portDescriptions
             = (Timestamped<List<PortDescription>>) kryo.readClassAndObject(input);
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java
index 5e1457a..f03600d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java
@@ -86,7 +86,7 @@
             final List<NodeId> standbyList = Collections.<NodeId>emptyList();
             eventDispatcher.post(new ReplicaInfoEvent(MASTER_CHANGED,
                                 event.subject(),
-                                new ReplicaInfo(event.node(), standbyList)));
+                                new ReplicaInfo(event.roleInfo().master(), standbyList)));
         }
     }
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/intent/impl/DistributedIntentStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/intent/impl/DistributedIntentStore.java
index 0deccfb..f940f7d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/intent/impl/DistributedIntentStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/intent/impl/DistributedIntentStore.java
@@ -82,12 +82,28 @@
     public IntentEvent setState(Intent intent, IntentState state) {
         IntentId id = intent.id();
         states.put(id, state);
-        IntentEvent.Type type = (state == SUBMITTED ? IntentEvent.Type.SUBMITTED :
-                (state == INSTALLED ? IntentEvent.Type.INSTALLED :
-                        (state == FAILED ? IntentEvent.Type.FAILED :
-                                state == WITHDRAWN ? IntentEvent.Type.WITHDRAWN :
-                                        null)));
-        return type == null ? null : new IntentEvent(type, intent);
+        IntentEvent.Type type = null;
+
+        switch (state) {
+        case SUBMITTED:
+            type = IntentEvent.Type.SUBMITTED;
+            break;
+        case INSTALLED:
+            type = IntentEvent.Type.INSTALLED;
+            break;
+        case FAILED:
+            type = IntentEvent.Type.FAILED;
+            break;
+        case WITHDRAWN:
+            type = IntentEvent.Type.WITHDRAWN;
+            break;
+        default:
+            break;
+        }
+        if (type == null) {
+            return null;
+        }
+        return new IntentEvent(type, intent);
     }
 
     @Override
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/DistributedStatisticStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/DistributedStatisticStore.java
new file mode 100644
index 0000000..273e3cc
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/DistributedStatisticStore.java
@@ -0,0 +1,300 @@
+package org.onlab.onos.store.statistic.impl;
+
+import static org.onlab.onos.store.statistic.impl.StatisticStoreMessageSubjects.*;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import com.google.common.collect.Sets;
+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.onos.cluster.ClusterService;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.flow.FlowEntry;
+import org.onlab.onos.net.flow.FlowRule;
+import org.onlab.onos.net.flow.instructions.Instruction;
+import org.onlab.onos.net.flow.instructions.Instructions;
+import org.onlab.onos.net.statistic.StatisticStore;
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.ClusterMessage;
+import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
+import org.onlab.onos.store.cluster.messaging.ClusterMessageResponse;
+import org.onlab.onos.store.flow.ReplicaInfo;
+import org.onlab.onos.store.flow.ReplicaInfoService;
+import org.onlab.onos.store.serializers.KryoNamespaces;
+import org.onlab.onos.store.serializers.KryoSerializer;
+import org.onlab.util.KryoNamespace;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * Maintains statistics using RPC calls to collect stats from remote instances
+ * on demand.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedStatisticStore implements StatisticStore {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private ReplicaInfoService replicaInfoManager;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private ClusterCommunicationService clusterCommunicator;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private ClusterService clusterService;
+
+    private Map<ConnectPoint, InternalStatisticRepresentation> representations =
+            new ConcurrentHashMap<>();
+
+    private Map<ConnectPoint, Set<FlowEntry>> previous =
+            new ConcurrentHashMap<>();
+
+    private Map<ConnectPoint, Set<FlowEntry>> current =
+            new ConcurrentHashMap<>();
+
+    protected static final KryoSerializer SERIALIZER = new KryoSerializer() {
+        @Override
+        protected void setupKryoPool() {
+            serializerPool = KryoNamespace.newBuilder()
+                    .register(KryoNamespaces.API)
+                    // register this store specific classes here
+                    .build()
+                    .populate(1);
+        }
+    };;
+
+    private static final long STATISTIC_STORE_TIMEOUT_MILLIS = 3000;
+
+    @Activate
+    public void activate() {
+        clusterCommunicator.addSubscriber(GET_CURRENT, new ClusterMessageHandler() {
+
+            @Override
+            public void handle(ClusterMessage message) {
+                ConnectPoint cp = SERIALIZER.decode(message.payload());
+                try {
+                    message.respond(SERIALIZER.encode(getCurrentStatisticInternal(cp)));
+                } catch (IOException e) {
+                    log.error("Failed to respond back", e);
+                }
+            }
+        });
+
+        clusterCommunicator.addSubscriber(GET_PREVIOUS, new ClusterMessageHandler() {
+
+            @Override
+            public void handle(ClusterMessage message) {
+                ConnectPoint cp = SERIALIZER.decode(message.payload());
+                try {
+                    message.respond(SERIALIZER.encode(getPreviousStatisticInternal(cp)));
+                } catch (IOException e) {
+                    log.error("Failed to respond back", e);
+                }
+            }
+        });
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public void prepareForStatistics(FlowRule rule) {
+        ConnectPoint cp = buildConnectPoint(rule);
+        if (cp == null) {
+            return;
+        }
+        InternalStatisticRepresentation rep;
+        synchronized (representations) {
+            rep = getOrCreateRepresentation(cp);
+        }
+        rep.prepare();
+    }
+
+    @Override
+    public synchronized void removeFromStatistics(FlowRule rule) {
+        ConnectPoint cp = buildConnectPoint(rule);
+        if (cp == null) {
+            return;
+        }
+        InternalStatisticRepresentation rep = representations.get(cp);
+        if (rep != null) {
+            rep.remove(rule);
+        }
+        Set<FlowEntry> values = current.get(cp);
+        if (values != null) {
+            values.remove(rule);
+        }
+        values = previous.get(cp);
+        if (values != null) {
+            values.remove(rule);
+        }
+
+    }
+
+    @Override
+    public void addOrUpdateStatistic(FlowEntry rule) {
+        ConnectPoint cp = buildConnectPoint(rule);
+        if (cp == null) {
+            return;
+        }
+        InternalStatisticRepresentation rep = representations.get(cp);
+        if (rep != null && rep.submit(rule)) {
+            updatePublishedStats(cp, rep.get());
+        }
+    }
+
+    private synchronized void updatePublishedStats(ConnectPoint cp,
+                                                   Set<FlowEntry> flowEntries) {
+        Set<FlowEntry> curr = current.get(cp);
+        if (curr == null) {
+            curr = new HashSet<>();
+        }
+        previous.put(cp, curr);
+        current.put(cp, flowEntries);
+
+    }
+
+    @Override
+    public Set<FlowEntry> getCurrentStatistic(ConnectPoint connectPoint) {
+        ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(connectPoint.deviceId());
+        if (replicaInfo.master().get().equals(clusterService.getLocalNode().id())) {
+            return getCurrentStatisticInternal(connectPoint);
+        } else {
+            ClusterMessage message = new ClusterMessage(
+                    clusterService.getLocalNode().id(),
+                    GET_CURRENT,
+                    SERIALIZER.encode(connectPoint));
+
+            try {
+                ClusterMessageResponse response =
+                        clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
+                return SERIALIZER.decode(response.get(STATISTIC_STORE_TIMEOUT_MILLIS,
+                                                      TimeUnit.MILLISECONDS));
+            } catch (IOException | TimeoutException e) {
+                // FIXME: throw a StatsStoreException
+                throw new RuntimeException(e);
+            }
+        }
+
+    }
+
+    private synchronized Set<FlowEntry> getCurrentStatisticInternal(ConnectPoint connectPoint) {
+        return current.get(connectPoint);
+    }
+
+    @Override
+    public Set<FlowEntry> getPreviousStatistic(ConnectPoint connectPoint) {
+        ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(connectPoint.deviceId());
+        if (replicaInfo.master().get().equals(clusterService.getLocalNode().id())) {
+            return getPreviousStatisticInternal(connectPoint);
+        } else {
+            ClusterMessage message = new ClusterMessage(
+                    clusterService.getLocalNode().id(),
+                    GET_PREVIOUS,
+                    SERIALIZER.encode(connectPoint));
+
+            try {
+                ClusterMessageResponse response =
+                        clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
+                return SERIALIZER.decode(response.get(STATISTIC_STORE_TIMEOUT_MILLIS,
+                                                      TimeUnit.MILLISECONDS));
+            } catch (IOException | TimeoutException e) {
+                // FIXME: throw a StatsStoreException
+                throw new RuntimeException(e);
+            }
+        }
+
+    }
+
+    private synchronized Set<FlowEntry> getPreviousStatisticInternal(ConnectPoint connectPoint) {
+        return previous.get(connectPoint);
+    }
+
+    private InternalStatisticRepresentation getOrCreateRepresentation(ConnectPoint cp) {
+
+        if (representations.containsKey(cp)) {
+            return representations.get(cp);
+        } else {
+            InternalStatisticRepresentation rep = new InternalStatisticRepresentation();
+            representations.put(cp, rep);
+            return rep;
+        }
+
+    }
+
+    private ConnectPoint buildConnectPoint(FlowRule rule) {
+        PortNumber port = getOutput(rule);
+        if (port == null) {
+            log.warn("Rule {} has no output.", rule);
+            return null;
+        }
+        ConnectPoint cp = new ConnectPoint(rule.deviceId(), port);
+        return cp;
+    }
+
+    private PortNumber getOutput(FlowRule rule) {
+        for (Instruction i : rule.treatment().instructions()) {
+            if (i.type() == Instruction.Type.OUTPUT) {
+                Instructions.OutputInstruction out = (Instructions.OutputInstruction) i;
+                return out.port();
+            }
+            if (i.type() == Instruction.Type.DROP) {
+                return PortNumber.P0;
+            }
+        }
+        return null;
+    }
+
+    private class InternalStatisticRepresentation {
+
+        private final AtomicInteger counter = new AtomicInteger(0);
+        private final Set<FlowEntry> rules = new HashSet<>();
+
+        public void prepare() {
+            counter.incrementAndGet();
+        }
+
+        public synchronized void remove(FlowRule rule) {
+            rules.remove(rule);
+            counter.decrementAndGet();
+        }
+
+        public synchronized boolean submit(FlowEntry rule) {
+            if (rules.contains(rule)) {
+                rules.remove(rule);
+            }
+            rules.add(rule);
+            if (counter.get() == 0) {
+                return true;
+            } else {
+                return counter.decrementAndGet() == 0;
+            }
+        }
+
+        public synchronized Set<FlowEntry> get() {
+            counter.set(rules.size());
+            return Sets.newHashSet(rules);
+        }
+
+
+    }
+
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/StatisticStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/StatisticStoreMessageSubjects.java
new file mode 100644
index 0000000..a096a3d
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/StatisticStoreMessageSubjects.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.store.statistic.impl;
+
+import org.onlab.onos.store.cluster.messaging.MessageSubject;
+
+/**
+ * MessageSubjects used by DistributedStatisticStore peer-peer communication.
+ */
+public final class StatisticStoreMessageSubjects {
+    private StatisticStoreMessageSubjects() {}
+        public static final MessageSubject GET_CURRENT =
+                new MessageSubject("peer-return-current");
+        public static final MessageSubject GET_PREVIOUS =
+            new MessageSubject("peer-return-previous");
+
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/package-info.java
new file mode 100644
index 0000000..122d2be
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/statistic/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of the statistic store.
+ */
+package org.onlab.onos.store.statistic.impl;
\ No newline at end of file
diff --git a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
index 74ca8cd..316a3b4 100644
--- a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
+++ b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
@@ -91,23 +91,14 @@
 
     @Override
     public MastershipRole getRole(NodeId nodeId, DeviceId deviceId) {
-        NodeId current = getNode(MASTER, deviceId);
-        if (current == null) {
-            if (isRole(STANDBY, nodeId, deviceId)) {
-                //was previously standby, or set to standby from master
-                return MastershipRole.STANDBY;
-            } else {
-                return MastershipRole.NONE;
-            }
-        } else {
-            if (current.equals(nodeId)) {
-                //*should* be in unusable, not always
-                return MastershipRole.MASTER;
-            } else {
-                //may be in backups or unusable from earlier retirement
-                return MastershipRole.STANDBY;
-            }
+        final RoleValue roleInfo = getRoleValue(deviceId);
+        if (roleInfo.contains(MASTER, nodeId)) {
+            return MASTER;
         }
+        if (roleInfo.contains(STANDBY, nodeId)) {
+            return STANDBY;
+        }
+        return NONE;
     }
 
     @Override
@@ -124,10 +115,11 @@
                     roleMap.put(deviceId, rv);
                     return null;
                 case STANDBY:
+                case NONE:
                     NodeId current = rv.get(MASTER);
                     if (current != null) {
                         //backup and replace current master
-                        rv.reassign(nodeId, NONE, STANDBY);
+                        rv.reassign(current, NONE, STANDBY);
                         rv.replace(current, nodeId, MASTER);
                     } else {
                         //no master before so just add.
@@ -137,12 +129,6 @@
                     roleMap.put(deviceId, rv);
                     updateTerm(deviceId);
                     return new MastershipEvent(MASTER_CHANGED, deviceId, rv.roleInfo());
-                case NONE:
-                    rv.add(MASTER, nodeId);
-                    rv.reassign(nodeId, STANDBY, NONE);
-                    roleMap.put(deviceId, rv);
-                    updateTerm(deviceId);
-                    return new MastershipEvent(MASTER_CHANGED, deviceId, rv.roleInfo());
                 default:
                     log.warn("unknown Mastership Role {}", role);
                     return null;
@@ -193,21 +179,28 @@
             switch (role) {
                 case MASTER:
                     rv.reassign(local, STANDBY, NONE);
+                    terms.putIfAbsent(deviceId, INIT);
                     roleMap.put(deviceId, rv);
                     break;
                 case STANDBY:
                     rv.reassign(local, NONE, STANDBY);
                     roleMap.put(deviceId, rv);
                     terms.putIfAbsent(deviceId, INIT);
-
                     break;
                 case NONE:
-                    //claim mastership
-                    rv.add(MASTER, local);
-                    rv.reassign(local, STANDBY, NONE);
+                    //either we're the first standby, or first to device.
+                    //for latter, claim mastership.
+                    if (rv.get(MASTER) == null) {
+                        rv.add(MASTER, local);
+                        rv.reassign(local, STANDBY, NONE);
+                        updateTerm(deviceId);
+                        role = MastershipRole.MASTER;
+                    } else {
+                        rv.add(STANDBY, local);
+                        rv.reassign(local, NONE, STANDBY);
+                        role = MastershipRole.STANDBY;
+                    }
                     roleMap.put(deviceId, rv);
-                    updateTerm(deviceId);
-                    role = MastershipRole.MASTER;
                     break;
                 default:
                     log.warn("unknown Mastership Role {}", role);
@@ -315,7 +308,10 @@
         RoleValue value = roleMap.get(deviceId);
         if (value == null) {
             value = new RoleValue();
-            roleMap.put(deviceId, value);
+            RoleValue concurrentlyAdded = roleMap.putIfAbsent(deviceId, value);
+            if (concurrentlyAdded != null) {
+                return concurrentlyAdded;
+            }
         }
         return value;
     }
@@ -329,16 +325,6 @@
         return null;
     }
 
-    //check if node is a certain role given a device
-    private boolean isRole(
-            MastershipRole role, NodeId nodeId, DeviceId deviceId) {
-        RoleValue value = roleMap.get(deviceId);
-        if (value != null) {
-            return value.contains(role, nodeId);
-        }
-        return false;
-    }
-
     //adds or updates term information.
     private void updateTerm(DeviceId deviceId) {
         terms.lock(deviceId);
diff --git a/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java b/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
index 5c867f4..c5daf6c 100644
--- a/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
+++ b/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
@@ -97,6 +97,7 @@
         assertEquals("wrong role:", NONE, dms.getRole(N1, DID1));
         testStore.put(DID1, N1, true, false, true);
         assertEquals("wrong role:", MASTER, dms.getRole(N1, DID1));
+        testStore.put(DID1, N2, false, true, false);
         assertEquals("wrong role:", STANDBY, dms.getRole(N2, DID1));
     }
 
@@ -155,6 +156,7 @@
 
         //switch over to N2
         assertEquals("wrong event:", Type.MASTER_CHANGED, dms.setMaster(N2, DID1).type());
+        System.out.println(dms.getTermFor(DID1).master() + ":" + dms.getTermFor(DID1).termNumber());
         assertEquals("wrong term", MastershipTerm.of(N2, 1), dms.getTermFor(DID1));
 
         //orphan switch - should be rare case
@@ -182,14 +184,9 @@
         assertEquals("wrong event:", Type.MASTER_CHANGED, dms.relinquishRole(N1, DID1).type());
         assertEquals("wrong master", N2, dms.getMaster(DID1));
 
-        //STANDBY - nothing here, either
-        assertNull("wrong event:", dms.relinquishRole(N1, DID1));
-        assertEquals("wrong role for node:", STANDBY, dms.getRole(N1, DID1));
-
         //all nodes "give up" on device, which goes back to NONE.
         assertNull("wrong event:", dms.relinquishRole(N2, DID1));
         assertEquals("wrong role for node:", NONE, dms.getRole(N2, DID1));
-        assertEquals("wrong role for node:", NONE, dms.getRole(N1, DID1));
 
         assertEquals("wrong number of retired nodes", 2,
                 dms.roleMap.get(DID1).nodesOfRole(NONE).size());
@@ -201,6 +198,10 @@
         assertEquals("wrong number of backup nodes", 1,
                 dms.roleMap.get(DID1).nodesOfRole(STANDBY).size());
 
+        //If STANDBY, should drop to NONE
+        assertNull("wrong event:", dms.relinquishRole(N1, DID1));
+        assertEquals("wrong role for node:", NONE, dms.getRole(N1, DID1));
+
         //NONE - nothing happens
         assertNull("wrong event:", dms.relinquishRole(N1, DID2));
         assertEquals("wrong role for node:", NONE, dms.getRole(N1, DID2));
@@ -218,7 +219,7 @@
             public void notify(MastershipEvent event) {
                 assertEquals("wrong event:", Type.MASTER_CHANGED, event.type());
                 assertEquals("wrong subject", DID1, event.subject());
-                assertEquals("wrong subject", N1, event.node());
+                assertEquals("wrong subject", N1, event.roleInfo().master());
                 addLatch.countDown();
             }
         };
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
index e6259d8..7fddb01 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
@@ -4,6 +4,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.DefaultControllerNode;
@@ -30,6 +31,7 @@
 import org.onlab.onos.net.flow.DefaultFlowRule;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.FlowEntry;
 import org.onlab.onos.net.flow.FlowId;
 import org.onlab.onos.net.flow.StoredFlowEntry;
 import org.onlab.onos.net.flow.criteria.Criteria;
@@ -77,6 +79,7 @@
                     ArrayList.class,
                     Arrays.asList().getClass(),
                     HashMap.class,
+                    HashSet.class,
                     //
                     //
                     ControllerNode.State.class,
@@ -98,6 +101,8 @@
                     DefaultFlowEntry.class,
                     StoredFlowEntry.class,
                     DefaultFlowRule.class,
+                    DefaultFlowEntry.class,
+                    FlowEntry.FlowEntryState.class,
                     FlowId.class,
                     DefaultTrafficSelector.class,
                     Criteria.PortCriterion.class,
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
index fbfaf9d..c1be1f5 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
@@ -70,15 +70,16 @@
 
     public static final String DEVICE_NOT_FOUND = "Device with ID %s not found";
 
-    // collection of Description given from various providers
+    // Collection of Description given from various providers
     private final ConcurrentMap<DeviceId, Map<ProviderId, DeviceDescriptions>>
-                                deviceDescs = Maps.newConcurrentMap();
+            deviceDescs = Maps.newConcurrentMap();
 
-    // cache of Device and Ports generated by compositing descriptions from providers
+    // Cache of Device and Ports generated by compositing descriptions from providers
     private final ConcurrentMap<DeviceId, Device> devices = Maps.newConcurrentMap();
-    private final ConcurrentMap<DeviceId, ConcurrentMap<PortNumber, Port>> devicePorts = Maps.newConcurrentMap();
+    private final ConcurrentMap<DeviceId, ConcurrentMap<PortNumber, Port>>
+            devicePorts = Maps.newConcurrentMap();
 
-    // available(=UP) devices
+    // Available (=UP) devices
     private final Set<DeviceId> availableDevices = Sets.newConcurrentHashSet();
 
 
@@ -113,19 +114,17 @@
 
     @Override
     public DeviceEvent createOrUpdateDevice(ProviderId providerId,
-                                     DeviceId deviceId,
-                                     DeviceDescription deviceDescription) {
-
+                                            DeviceId deviceId,
+                                            DeviceDescription deviceDescription) {
         Map<ProviderId, DeviceDescriptions> providerDescs
-            = getOrCreateDeviceDescriptions(deviceId);
+                = getOrCreateDeviceDescriptions(deviceId);
 
         synchronized (providerDescs) {
             // locking per device
-
             DeviceDescriptions descs
-                = getOrCreateProviderDeviceDescriptions(providerDescs,
-                                                        providerId,
-                                                        deviceDescription);
+                    = getOrCreateProviderDeviceDescriptions(providerDescs,
+                                                            providerId,
+                                                            deviceDescription);
 
             Device oldDevice = devices.get(deviceId);
             // update description
@@ -145,12 +144,11 @@
     // Creates the device and returns the appropriate event if necessary.
     // Guarded by deviceDescs value (=Device lock)
     private DeviceEvent createDevice(ProviderId providerId, Device newDevice) {
-
         // update composed device cache
         Device oldDevice = devices.putIfAbsent(newDevice.id(), newDevice);
         verify(oldDevice == null,
-                "Unexpected Device in cache. PID:%s [old=%s, new=%s]",
-                providerId, oldDevice, newDevice);
+               "Unexpected Device in cache. PID:%s [old=%s, new=%s]",
+               providerId, oldDevice, newDevice);
 
         if (!providerId.isAncillary()) {
             availableDevices.add(newDevice.id());
@@ -162,17 +160,24 @@
     // Updates the device and returns the appropriate event if necessary.
     // Guarded by deviceDescs value (=Device lock)
     private DeviceEvent updateDevice(ProviderId providerId, Device oldDevice, Device newDevice) {
-
         // We allow only certain attributes to trigger update
-        if (!Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
-            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion()) ||
-            !AnnotationsUtil.isEqual(oldDevice.annotations(), newDevice.annotations())) {
+        boolean propertiesChanged =
+                !Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
+                        !Objects.equals(oldDevice.swVersion(), newDevice.swVersion());
+        boolean annotationsChanged =
+                !AnnotationsUtil.isEqual(oldDevice.annotations(), newDevice.annotations());
+
+        // Primary providers can respond to all changes, but ancillary ones
+        // should respond only to annotation changes.
+        if ((providerId.isAncillary() && annotationsChanged) ||
+                (!providerId.isAncillary() && (propertiesChanged || annotationsChanged))) {
 
             boolean replaced = devices.replace(newDevice.id(), oldDevice, newDevice);
             if (!replaced) {
+                // FIXME: Is the enclosing if required here?
                 verify(replaced,
-                        "Replacing devices cache failed. PID:%s [expected:%s, found:%s, new=%s]",
-                        providerId, oldDevice, devices.get(newDevice.id())
+                       "Replacing devices cache failed. PID:%s [expected:%s, found:%s, new=%s]",
+                       providerId, oldDevice, devices.get(newDevice.id())
                         , newDevice);
             }
             if (!providerId.isAncillary()) {
@@ -193,7 +198,7 @@
     @Override
     public DeviceEvent markOffline(DeviceId deviceId) {
         Map<ProviderId, DeviceDescriptions> providerDescs
-            = getOrCreateDeviceDescriptions(deviceId);
+                = getOrCreateDeviceDescriptions(deviceId);
 
         // locking device
         synchronized (providerDescs) {
@@ -212,9 +217,8 @@
 
     @Override
     public List<DeviceEvent> updatePorts(ProviderId providerId,
-                                      DeviceId deviceId,
-                                      List<PortDescription> portDescriptions) {
-
+                                         DeviceId deviceId,
+                                         List<PortDescription> portDescriptions) {
         Device device = devices.get(deviceId);
         checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
 
@@ -226,8 +230,8 @@
             DeviceDescriptions descs = descsMap.get(providerId);
             // every provider must provide DeviceDescription.
             checkArgument(descs != null,
-                    "Device description for Device ID %s from Provider %s was not found",
-                    deviceId, providerId);
+                          "Device description for Device ID %s from Provider %s was not found",
+                          deviceId, providerId);
 
             Map<PortNumber, Port> ports = getPortMap(deviceId);
 
@@ -247,8 +251,8 @@
                 newPort = composePort(device, number, descsMap);
 
                 events.add(oldPort == null ?
-                        createPort(device, newPort, ports) :
-                        updatePort(device, oldPort, newPort, ports));
+                                   createPort(device, newPort, ports) :
+                                   updatePort(device, oldPort, newPort, ports));
             }
 
             events.addAll(pruneOldPorts(device, ports, processed));
@@ -272,7 +276,7 @@
                                    Port newPort,
                                    Map<PortNumber, Port> ports) {
         if (oldPort.isEnabled() != newPort.isEnabled() ||
-            !AnnotationsUtil.isEqual(oldPort.annotations(), newPort.annotations())) {
+                !AnnotationsUtil.isEqual(oldPort.annotations(), newPort.annotations())) {
 
             ports.put(oldPort.number(), newPort);
             return new DeviceEvent(PORT_UPDATED, device, newPort);
@@ -303,7 +307,7 @@
     // exist, it creates and registers a new one.
     private ConcurrentMap<PortNumber, Port> getPortMap(DeviceId deviceId) {
         return createIfAbsentUnchecked(devicePorts, deviceId,
-                NewConcurrentHashMap.<PortNumber, Port>ifNeeded());
+                                       NewConcurrentHashMap.<PortNumber, Port>ifNeeded());
     }
 
     private Map<ProviderId, DeviceDescriptions> getOrCreateDeviceDescriptions(
@@ -325,9 +329,8 @@
 
     // Guarded by deviceDescs value (=Device lock)
     private DeviceDescriptions getOrCreateProviderDeviceDescriptions(
-                            Map<ProviderId, DeviceDescriptions> device,
-                            ProviderId providerId, DeviceDescription deltaDesc) {
-
+            Map<ProviderId, DeviceDescriptions> device,
+            ProviderId providerId, DeviceDescription deltaDesc) {
         synchronized (device) {
             DeviceDescriptions r = device.get(providerId);
             if (r == null) {
@@ -340,7 +343,7 @@
 
     @Override
     public DeviceEvent updatePortStatus(ProviderId providerId, DeviceId deviceId,
-                                 PortDescription portDescription) {
+                                        PortDescription portDescription) {
         Device device = devices.get(deviceId);
         checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
 
@@ -351,8 +354,8 @@
             DeviceDescriptions descs = descsMap.get(providerId);
             // assuming all providers must give DeviceDescription first
             checkArgument(descs != null,
-                    "Device description for Device ID %s from Provider %s was not found",
-                    deviceId, providerId);
+                          "Device description for Device ID %s from Provider %s was not found",
+                          deviceId, providerId);
 
             ConcurrentMap<PortNumber, Port> ports = getPortMap(deviceId);
             final PortNumber number = portDescription.portNumber();
@@ -404,19 +407,19 @@
             availableDevices.remove(deviceId);
             descs.clear();
             return device == null ? null :
-                new DeviceEvent(DEVICE_REMOVED, device, null);
+                    new DeviceEvent(DEVICE_REMOVED, device, null);
         }
     }
 
     /**
      * Returns a Device, merging description given from multiple Providers.
      *
-     * @param deviceId device identifier
+     * @param deviceId      device identifier
      * @param providerDescs Collection of Descriptions from multiple providers
      * @return Device instance
      */
     private Device composeDevice(DeviceId deviceId,
-            Map<ProviderId, DeviceDescriptions> providerDescs) {
+                                 Map<ProviderId, DeviceDescriptions> providerDescs) {
 
         checkArgument(!providerDescs.isEmpty(), "No Device descriptions supplied");
 
@@ -447,21 +450,21 @@
             annotations = merge(annotations, e.getValue().getDeviceDesc().annotations());
         }
 
-        return new DefaultDevice(primary, deviceId , type, manufacturer,
-                            hwVersion, swVersion, serialNumber,
-                            chassisId, annotations);
+        return new DefaultDevice(primary, deviceId, type, manufacturer,
+                                 hwVersion, swVersion, serialNumber,
+                                 chassisId, annotations);
     }
 
     /**
      * Returns a Port, merging description given from multiple Providers.
      *
-     * @param device device the port is on
-     * @param number port number
+     * @param device   device the port is on
+     * @param number   port number
      * @param descsMap Collection of Descriptions from multiple providers
      * @return Port instance
      */
     private Port composePort(Device device, PortNumber number,
-                Map<ProviderId, DeviceDescriptions> descsMap) {
+                             Map<ProviderId, DeviceDescriptions> descsMap) {
 
         ProviderId primary = pickPrimaryPID(descsMap);
         DeviceDescriptions primDescs = descsMap.get(primary);
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
index 3d10d3d..bbfc263 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
@@ -187,17 +187,23 @@
     public void deleteFlowRule(FlowRule rule) {
 
         List<StoredFlowEntry> entries = getFlowEntries(rule.deviceId(), rule.id());
+
         synchronized (entries) {
             for (StoredFlowEntry entry : entries) {
                 if (entry.equals(rule)) {
                     synchronized (entry) {
                         entry.setState(FlowEntryState.PENDING_REMOVE);
                         // TODO: Should we notify only if it's "remote" event?
-                        //notifyDelegate(new FlowRuleEvent(Type.RULE_REMOVE_REQUESTED, rule));
+                        notifyDelegate(FlowRuleBatchEvent.create(
+                                new FlowRuleBatchRequest(
+                                        Collections.<FlowEntry>emptyList(),
+                                        Arrays.<FlowEntry>asList(entry))));
                     }
                 }
             }
         }
+
+
         //log.warn("Cannot find rule {}", rule);
     }
 
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleIntentStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleIntentStore.java
index 0ec8612..12e62e2 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleIntentStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleIntentStore.java
@@ -82,12 +82,28 @@
     public IntentEvent setState(Intent intent, IntentState state) {
         IntentId id = intent.id();
         states.put(id, state);
-        IntentEvent.Type type = (state == SUBMITTED ? IntentEvent.Type.SUBMITTED :
-                (state == INSTALLED ? IntentEvent.Type.INSTALLED :
-                        (state == FAILED ? IntentEvent.Type.FAILED :
-                                state == WITHDRAWN ? IntentEvent.Type.WITHDRAWN :
-                                        null)));
-        return type == null ? null : new IntentEvent(type, intent);
+        IntentEvent.Type type = null;
+
+        switch (state) {
+        case SUBMITTED:
+            type = IntentEvent.Type.SUBMITTED;
+            break;
+        case INSTALLED:
+            type = IntentEvent.Type.INSTALLED;
+            break;
+        case FAILED:
+            type = IntentEvent.Type.FAILED;
+            break;
+        case WITHDRAWN:
+            type = IntentEvent.Type.WITHDRAWN;
+            break;
+        default:
+            break;
+        }
+        if (type == null) {
+            return null;
+        }
+        return new IntentEvent(type, intent);
     }
 
     @Override
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/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
index 6fd02bc..1375a20 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
@@ -110,8 +110,7 @@
      *
      * @param role the failed role
      */
-    void returnRoleAssertFailure(RoleState role);
-
+    public void returnRoleAssertFailure(RoleState role);
 
     /**
      * Indicates if this switch is optical.
@@ -120,5 +119,4 @@
      */
     public boolean isOptical();
 
-
 }
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
index 53217da..33e5bdf 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
@@ -20,6 +20,12 @@
     public void switchRemoved(Dpid dpid);
 
     /**
+     * Notify that the switch has changed in some way.
+     * @param dpid the switch that changed
+     */
+    public void switchChanged(Dpid dpid);
+
+    /**
      * Notify that a port has changed.
      * @param dpid the switch on which the change happened.
      * @param status the new state of the port.
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
index 009cd3f..4ea2f71 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
@@ -41,7 +41,6 @@
 import org.projectfloodlight.openflow.protocol.OFHelloElem;
 import org.projectfloodlight.openflow.protocol.OFMessage;
 import org.projectfloodlight.openflow.protocol.OFPacketIn;
-import org.projectfloodlight.openflow.protocol.OFPacketOut;
 import org.projectfloodlight.openflow.protocol.OFPortDescStatsReply;
 import org.projectfloodlight.openflow.protocol.OFPortDescStatsRequest;
 import org.projectfloodlight.openflow.protocol.OFPortStatus;
@@ -565,6 +564,9 @@
             @Override
             void processOFStatisticsReply(OFChannelHandler h,
                     OFStatsReply m) {
+                if (m.getStatsType().equals(OFStatsType.PORT_DESC)) {
+                    h.sw.setPortDescReply((OFPortDescStatsReply) m);
+                }
                 h.dispatchMessage(m);
             }
 
@@ -608,6 +610,12 @@
                 h.dispatchMessage(m);
             }
 
+            @Override
+            void processOFFeaturesReply(OFChannelHandler h, OFFeaturesReply  m) {
+                h.sw.setFeaturesReply(m);
+                h.dispatchMessage(m);
+            }
+
         };
 
         private final boolean handshakeComplete;
@@ -652,10 +660,9 @@
          * However, we could be more forgiving
          * @param h the channel handler that received the message
          * @param m the message
-         * @throws SwitchStateException
-         * @throws SwitchStateExeption we always through the execption
+         * @throws SwitchStateException we always throw the exception
          */
-        // needs to be protected because enum members are acutally subclasses
+        // needs to be protected because enum members are actually subclasses
         protected void illegalMessageReceived(OFChannelHandler h, OFMessage m)
                 throws SwitchStateException {
             String msg = getSwitchStateMessage(h, m,
@@ -1016,7 +1023,9 @@
                 // all state for the original switch (with the same dpid),
                 // which we obviously don't want.
                 log.info("{}:removal called", getSwitchInfoString());
-                sw.removeConnectedSwitch();
+                if (sw != null) {
+                    sw.removeConnectedSwitch();
+                }
             } else {
                 // A duplicate was disconnected on this ChannelHandler,
                 // this is the same switch reconnecting, but the original state was
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OpenFlowControllerImpl.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OpenFlowControllerImpl.java
index 565ccb9..9ef3077 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OpenFlowControllerImpl.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OpenFlowControllerImpl.java
@@ -27,6 +27,8 @@
 import org.projectfloodlight.openflow.protocol.OFMessage;
 import org.projectfloodlight.openflow.protocol.OFPacketIn;
 import org.projectfloodlight.openflow.protocol.OFPortStatus;
+import org.projectfloodlight.openflow.protocol.OFStatsReply;
+import org.projectfloodlight.openflow.protocol.OFStatsType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -146,6 +148,11 @@
                 l.portChanged(dpid, (OFPortStatus) msg);
             }
             break;
+        case FEATURES_REPLY:
+            for (OpenFlowSwitchListener l : ofSwitchListener) {
+                l.switchChanged(dpid);
+            }
+            break;
         case PACKET_IN:
             OpenFlowPacketContext pktCtx = DefaultOpenFlowPacketContext
             .packetContextFromPacketIn(this.getSwitch(dpid),
@@ -154,9 +161,15 @@
                 p.handlePacket(pktCtx);
             }
             break;
+        case STATS_REPLY:
+            OFStatsReply reply = (OFStatsReply) msg;
+            if (reply.getStatsType().equals(OFStatsType.PORT_DESC)) {
+                for (OpenFlowSwitchListener l : ofSwitchListener) {
+                    l.switchChanged(dpid);
+                }
+            }
         case FLOW_REMOVED:
         case ERROR:
-        case STATS_REPLY:
         case BARRIER_REPLY:
             executor.submit(new OFMessageHandler(dpid, msg));
             break;
@@ -194,7 +207,7 @@
                         + "value for dpid: {}", dpid);
                 return false;
             } else {
-                log.error("Added switch {}", dpid);
+                log.info("Added switch {}", dpid);
                 connectedSwitches.put(dpid, sw);
                 for (OpenFlowSwitchListener l : ofSwitchListener) {
                     l.switchAdded(dpid);
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFSwitchImplCPqD13.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFSwitchImplCPqD13.java
index c4c2e19..3d60dfa 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFSwitchImplCPqD13.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFSwitchImplCPqD13.java
@@ -1188,7 +1188,8 @@
                 .setHardTimeout(0)
                 .setXid(getNextTransactionId())
                 .build();
-        sendMsg(tableMissEntry);
+
+        write(tableMissEntry);
     }
 
     private void sendBarrier(boolean finalBarrier) {
@@ -1200,7 +1201,8 @@
                 .buildBarrierRequest()
                 .setXid(xid)
                 .build();
-        sendMsg(br);
+
+        write(br);
     }
 
     @Override
@@ -1210,7 +1212,7 @@
 
     @Override
     public void write(OFMessage msg) {
-        this.channel.write(msg);
+        this.channel.write(Collections.singletonList(msg));
 
     }
 
diff --git a/pom.xml b/pom.xml
index a9c83fd..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>
@@ -610,7 +609,8 @@
             </plugin>
 
         </plugins>
-
     </reporting>
-
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
 </project>
diff --git a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
index 75ce1da..bf4fee0 100644
--- a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
+++ b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
@@ -16,6 +16,7 @@
 package org.onlab.onos.provider.lldp.impl;
 
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static org.slf4j.LoggerFactory.getLogger;
 
 import java.nio.ByteBuffer;
@@ -95,11 +96,13 @@
      */
     public LinkDiscovery(Device device, PacketService pktService,
                          MastershipService masterService, LinkProviderService providerService, Boolean... useBDDP) {
+
         this.device = device;
         this.probeRate = 3000;
         this.linkProvider = providerService;
         this.pktService = pktService;
-        this.mastershipService = masterService;
+
+        this.mastershipService = checkNotNull(masterService, "WTF!");
         this.slowPorts = Collections.synchronizedSet(new HashSet<Long>());
         this.fastPorts = Collections.synchronizedSet(new HashSet<Long>());
         this.portProbeCount = new HashMap<>();
@@ -344,7 +347,14 @@
     }
 
     private void sendProbes(Long portNumber) {
-       if (mastershipService.getLocalRole(this.device.id()) ==
+       if (device == null) {
+           log.warn("CRAZY SHIT");
+       }
+       if (mastershipService == null) {
+           log.warn("INSANE");
+       }
+       if (device.type() != Device.Type.ROADM &&
+               mastershipService.getLocalRole(this.device.id()) ==
                MastershipRole.MASTER) {
            OutboundPacket pkt = this.createOutBoundLLDP(portNumber);
            pktService.emit(pkt);
diff --git a/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java b/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
index fcc7810..984e8ae 100644
--- a/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
+++ b/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
@@ -23,7 +23,9 @@
 import org.onlab.onos.openflow.controller.OpenFlowSwitch;
 import org.onlab.onos.openflow.controller.OpenFlowSwitchListener;
 import org.onlab.onos.openflow.controller.RoleState;
+import org.onlab.onos.openflow.controller.driver.OpenFlowSwitchDriver;
 import org.onlab.packet.ChassisId;
+import org.projectfloodlight.openflow.protocol.OFFactory;
 import org.projectfloodlight.openflow.protocol.OFPortConfig;
 import org.projectfloodlight.openflow.protocol.OFPortDesc;
 import org.projectfloodlight.openflow.protocol.OFPortState;
@@ -89,6 +91,28 @@
     @Override
     public void triggerProbe(Device device) {
         LOG.info("Triggering probe on device {}", device.id());
+
+        // 1. check device liveness
+        // FIXME if possible, we might want this to be part of
+        // OpenFlowSwitch interface so the driver interface isn't misused.
+        OpenFlowSwitch sw = controller.getSwitch(dpid(device.id().uri()));
+        if (!((OpenFlowSwitchDriver) sw).isConnected()) {
+            providerService.deviceDisconnected(device.id());
+            return;
+        }
+
+        // 2. Prompt an update of port information. Do we have an XID for this?
+        OFFactory fact = sw.factory();
+        switch (fact.getVersion()) {
+            case OF_10:
+                sw.sendMsg(fact.buildFeaturesRequest().setXid(0).build());
+                break;
+            case OF_13:
+                sw.sendMsg(fact.buildPortDescStatsRequest().setXid(0).build());
+                break;
+            default:
+                LOG.warn("Unhandled protocol version");
+        }
     }
 
     @Override
@@ -141,6 +165,17 @@
             providerService.deviceDisconnected(deviceId(uri(dpid)));
         }
 
+
+        @Override
+        public void switchChanged(Dpid dpid) {
+            if (providerService == null) {
+                return;
+            }
+            DeviceId did = deviceId(uri(dpid));
+            OpenFlowSwitch sw = controller.getSwitch(dpid);
+            providerService.updatePorts(did, buildPortDescriptions(sw.getPorts()));
+        }
+
         @Override
         public void portChanged(Dpid dpid, OFPortStatus status) {
             PortDescription portDescription = buildPortDescription(status.getDesc());
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
index b7d84f0..8d3c018 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
@@ -31,7 +31,7 @@
 import org.onlab.onos.net.flow.FlowRuleProvider;
 import org.onlab.onos.net.flow.FlowRuleProviderRegistry;
 import org.onlab.onos.net.flow.FlowRuleProviderService;
-import org.onlab.onos.net.intent.BatchOperation;
+import org.onlab.onos.net.flow.BatchOperation;
 import org.onlab.onos.net.provider.AbstractProvider;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.net.topology.TopologyService;
@@ -107,6 +107,8 @@
     private final Map<Long, InstallationFuture> pendingFMs =
             new ConcurrentHashMap<Long, InstallationFuture>();
 
+    private final Map<Dpid, FlowStatsCollector> collectors = Maps.newHashMap();
+
     /**
      * Creates an OpenFlow host provider.
      */
@@ -119,6 +121,14 @@
         providerService = providerRegistry.register(this);
         controller.addListener(listener);
         controller.addEventListener(listener);
+
+        for (OpenFlowSwitch sw : controller.getSwitches()) {
+            FlowStatsCollector fsc = new FlowStatsCollector(sw, POLL_INTERVAL);
+            fsc.start();
+            collectors.put(new Dpid(sw.getId()), fsc);
+        }
+
+
         log.info("Started");
     }
 
@@ -217,7 +227,7 @@
     private class InternalFlowProvider
     implements OpenFlowSwitchListener, OpenFlowEventListener {
 
-        private final Map<Dpid, FlowStatsCollector> collectors = Maps.newHashMap();
+
         private final Multimap<DeviceId, FlowEntry> completeEntries =
                 ArrayListMultimap.create();
 
@@ -237,6 +247,10 @@
         }
 
         @Override
+        public void switchChanged(Dpid dpid) {
+        }
+
+        @Override
         public void portChanged(Dpid dpid, OFPortStatus status) {
             //TODO: Decide whether to evict flows internal store.
         }
@@ -317,6 +331,7 @@
             }
             return false;
         }
+
     }
 
     private class InstallationFuture implements ListenableFuture<CompletedBatchOperation> {
diff --git a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
index 7f16eaa..c3f5a68 100644
--- a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
+++ b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
@@ -118,6 +118,12 @@
                     DeviceId.deviceId("of:" + Long.toHexString(dpid.value())));
         }
 
+
+        @Override
+        public void switchChanged(Dpid dpid) {
+            //might not need to do anything since DeviceManager is notified
+        }
+
         @Override
         public void portChanged(Dpid dpid, OFPortStatus status) {
             LinkDiscovery ld = discoverers.get(dpid);
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/tools/test/topos/onos.py b/tools/test/topos/onos.py
index ba09f67..9566d00 100755
--- a/tools/test/topos/onos.py
+++ b/tools/test/topos/onos.py
@@ -16,58 +16,95 @@
 import time
 from sys import argv
 from time import sleep
+from sets import Set
 
 class ONOS( Controller ):
-    #def __init__( self, name, command='/opt/onos/bin/onos-service', **kwargs ):
-    #    Controller.__init__( self, name, command=command, inNamespace=True,  **kwargs )
-    #def __init__( self, name, inNamespace=False, command='controller',
-    #          cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1",
-    #          port=6633, protocol='tcp', **params ):
-    #self.command = command
-    #self.cargs = cargs
-    #self.cdir = cdir
-    #self.ip = ip
-    #self.port = port
-    #self.protocol = protocol
-    #Node.__init__( self, name, inNamespace=inNamespace,
-    #               ip=ip, **params  )
-    #self.checkListening()
-    
-    ONOS_DIR = '/opt/onos/'
-    KARAF_DIR = ONOS_DIR + 'apache-karaf-3.0.1/'
-    reactive = True
+    "TODO"
+
+    onosDir = '/opt/onos/'
+
+    def __init__( self, name, onosDir=onosDir,
+                  reactive=True, features=[ 'onos-app-tvue' ],
+                  **kwargs ):
+        '''TODO'''
+
+        Controller.__init__( self, name, **kwargs )
+        # the following have been done for us:
+        #self.ip = ip ('127.0.0.1')
+        #self.port = port (6633)
+        #self.protocol = protocol ('tcp')
+        #self.checkListening()
+
+        self.onosDir = onosDir
+        self.karafDir = onosDir + 'apache-karaf-3.0.1/'
+        self.instanceDir = self.karafDir
+
+        # add default modules
+        # TODO: consider an ordered set
+        self.features = Set([ 'webconsole',
+                              'onos-api',
+                              'onos-cli',
+                              'onos-openflow' ])
+        self.features.update( features )
+        # add reactive forwarding modules
+        if reactive:
+            self.features.update( ['onos-app-fwd', 
+                                   'onos-app-proxyarp',
+                                   'onos-app-mobility' ] )
+        # add the distributed core if we are in a namespace with no trivial core
+        if self.inNamespace and 'onos-core-trivial' not in self.features:
+            self.features.add( 'onos-core' )
+        # if there is no core, add the trivial one
+        if 'onos-core' not in self.features:
+            self.features.add( 'onos-core-trivial' )
+        print self.features  
    
     def start( self ):
-        # switch to the non-root user because karaf gets upset otherwise
-        # TODO we should look into why.... 
-        self.sendCmd( 'sudo su - %s' % self.findUser() )
-        self.waiting = False
-
         if self.inNamespace:
-            self.cmd( self.KARAF_DIR + 'bin/instance create %s' % self.name )
-            src  = self.KARAF_DIR + 'etc/org.apache.karaf.features.cfg'
-            dst = self.KARAF_DIR + 'instances/%s/etc/org.apache.karaf.features.cfg' % self.name
-            self.cmd( 'cp %s %s' % (src, dst) )
-            self.updateProperties( dst )
-            self.cmd( self.KARAF_DIR + 'bin/instance start %s' % self.name )
+            instanceOpts = ( '-furl mvn:org.onlab.onos/onos-features/1.0.0-SNAPSHOT/xml/features '
+                             '-s 8101' )
+            self.userCmd( self.karafDir + 'bin/instance create %s %s' % ( instanceOpts, self.name ) )
+            self.instanceDir = self.karafDir + 'instances/%s/' % self.name
         else:
             # we are running in the root namespace, so let's use the root instance
-            self.cmd( 'rm -rf '+ self.KARAF_DIR + 'data/' )
-            filename = self.KARAF_DIR + 'etc/org.apache.karaf.features.cfg'
-            self.updateProperties( filename )
-            self.cmd( self.KARAF_DIR + 'bin/start' )
+            # clean up the data directory
+            #self.userCmd( 'rm -rf '+ self.karafDir + 'data/' )
+            pass
 
+        self.userCmd( 'rm -rf '+ self.instanceDir + 'data/' )
+
+        # Update etc/org.apache.karaf.features.cfg
+        self.updateFeatures()
+
+        # TODO 2. Update etc/hazelcast.xml : interface lines
+        #cp etc/hazelcast.xml instances/c1/etc/
+        self.updateHazelcast()
+
+        # TODO 3. Update etc/system.properties : onos.ip
+        # TODO 4. Update config/cluster.json : with all nodes
+
+        # start onos
+        self.userCmd( self.instanceDir + 'bin/start' )
         #TODO we should wait for startup...
 
     def stop( self ):
-        if self.inNamespace:
-            self.cmd( '/opt/onos/apache-karaf-3.0.1/bin/instance stop %s' % self.name )
-            self.cmd( '/opt/onos/apache-karaf-3.0.1/bin/instance destroy %s' % self.name )
-        else:
-            self.cmd( self.ONOS_DIR + 'apache-karaf-3.0.1/bin/stop' )
+        self.userCmd( self.instanceDir + 'bin/stop' )
+        #if self.inNamespace:
+        #    self.userCmd( self.karafDir + 'bin/instance destroy %s' % self.name )
         self.terminate()
 
-    def updateProperties( self, filename ):
+    def updateHazelcast( self ):
+        readfile = self.karafDir + 'etc/hazelcast.xml'
+        writefile = self.instanceDir + 'etc/hazelcast.xml'
+        with open( readfile, 'r' ) as r:
+            with open( writefile, 'w' ) as w:
+                for line in r.readlines():
+                    if '<interface>' in line:
+                        line = '<interface>' + '192.168.123.*' + '</interface>\n'
+                    w.write( line )
+
+    def updateFeatures( self ):
+        filename = self.instanceDir + 'etc/org.apache.karaf.features.cfg'
         with open( filename, 'r+' ) as f:
             lines = f.readlines()
             f.seek(0)
@@ -75,17 +112,25 @@
             for line in lines:
                 #print '?', line,
                 if 'featuresBoot=' in line:
-                    line = line.rstrip()
-                    #print ord(line[-1]), ord(line[-2]), ord(line[-3])
-                    if self.reactive:
-                        line += ',onos-app-fwd'
-                    line += '\n'
+                    # parse the features from the line
+                    features = line.rstrip().split('=')[1].split(',')
+                    # add the features to our features set
+                    self.features.update( features )
+                    # generate the new features line
+                    line = 'featuresBoot=' + ','.join( self.features ) + '\n'
                     #print '!', line,
                 f.write( line )
 
+
     @classmethod
     def isAvailable( self ):
-        return quietRun( 'ls /opt/onos' )
+        return quietRun( 'ls %s' % self.onosDir )
+
+    def userCmd( self, cmd ):
+        # switch to the non-root user because karaf gets upset otherwise
+        # because the .m2repo is not stored with root
+        cmd = 'sudo -u %s %s' % ( self.findUser(), cmd )
+        return self.cmd( cmd )
 
     @staticmethod
     def findUser():
@@ -111,7 +156,7 @@
         # Connect everything to a single switch
         cs0 = self.addSwitch( 'cs0' )
         # Add hosts which will serve as data network controllers
-        for i in range( 0, n ):
+        for i in range( 1, n+1 ):
             c = self.addHost( 'c%s' % i, cls=dataController,
                               inNamespace=True )
             self.addLink( c, cs0 )
@@ -122,7 +167,7 @@
 
 class ONOSCluster( Controller ):
     # TODO
-    n = 4
+    n = 3
    
     def start( self ):
         ctopo = ControlNetwork( n=self.n, dataController=ONOS )
@@ -137,6 +182,9 @@
                 host.start()
 
     def stop( self ):
+        for host in self.cnet.hosts:
+            if isinstance( host, Controller ):
+                host.stop()
         self.cnet.stop()
         
     def clist( self ):
@@ -158,10 +206,11 @@
 
 if __name__ == '__main__':
     # Simple test for ONOS() controller class
-    setLogLevel( 'info' )
+    setLogLevel( 'info' ) #TODO info
     size = 2 if len( argv ) != 2 else int( argv[ 1 ] )
     net = Mininet( topo=LinearTopo( size ),
-                   controller=partial( ONOSCluster, n=4 ),
+                   #controller=ONOS,
+                   controller=partial( ONOSCluster, n=3 ), #TODO
                    switch=OVSSwitchONOS )
     net.start()
     #waitConnected( net.switches )
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/utils/osgi/src/main/java/org/onlab/osgi/TestServiceDirectory.java b/utils/osgi/src/main/java/org/onlab/osgi/TestServiceDirectory.java
index 2915d4b..7fecbc3 100644
--- a/utils/osgi/src/main/java/org/onlab/osgi/TestServiceDirectory.java
+++ b/utils/osgi/src/main/java/org/onlab/osgi/TestServiceDirectory.java
@@ -6,6 +6,7 @@
 /**
  * Service directory implementation suitable for testing.
  */
+@SuppressWarnings("unchecked")
 public class TestServiceDirectory implements ServiceDirectory {
 
     private ClassToInstanceMap<Object> services = MutableClassToInstanceMap.create();
diff --git a/web/api/pom.xml b/web/api/pom.xml
index 11d90cc..da8fd1c 100644
--- a/web/api/pom.xml
+++ b/web/api/pom.xml
@@ -23,13 +23,6 @@
             <version>1.0.0-SNAPSHOT</version>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <version>17.0</version>
-            <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/gui/src/main/webapp/geometry.js b/web/gui/src/main/webapp/geometry.js
new file mode 100644
index 0000000..71533cb
--- /dev/null
+++ b/web/gui/src/main/webapp/geometry.js
@@ -0,0 +1,107 @@
+/*
+ Geometry library - based on work by Mike Bostock.
+ */
+
+(function() {
+
+    if (typeof geo == 'undefined') {
+        geo = {};
+    }
+
+    var tolerance = 1e-10;
+
+    function eq(a, b) {
+        return (Math.abs(a - b) < tolerance);
+    }
+
+    function gt(a, b) {
+        return (a - b > -tolerance);
+    }
+
+    function lt(a, b) {
+        return gt(b, a);
+    }
+
+    geo.eq = eq;
+    geo.gt = gt;
+    geo.lt = lt;
+
+    geo.LineSegment = function(x1, y1, x2, y2) {
+        this.x1 = x1;
+        this.y1 = y1;
+        this.x2 = x2;
+        this.y2 = y2;
+
+        // Ax + By = C
+        this.a = y2 - y1;
+        this.b = x1 - x2;
+        this.c = x1 * this.a + y1 * this.b;
+
+        if (eq(this.a, 0) && eq(this.b, 0)) {
+            throw new Error(
+                'Cannot construct a LineSegment with two equal endpoints.');
+        }
+    };
+
+    geo.LineSegment.prototype.intersect = function(that) {
+        var d = (this.x1 - this.x2) * (that.y1 - that.y2) -
+            (this.y1 - this.y2) * (that.x1 - that.x2);
+
+        if (eq(d, 0)) {
+            // The two lines are parallel or very close.
+            return {
+                x : NaN,
+                y : NaN
+            };
+        }
+
+        var t1  = this.x1 * this.y2 - this.y1 * this.x2,
+            t2  = that.x1 * that.y2 - that.y1 * that.x2,
+            x   = (t1 * (that.x1 - that.x2) - t2 * (this.x1 - this.x2)) / d,
+            y   = (t1 * (that.y1 - that.y2) - t2 * (this.y1 - this.y2)) / d,
+            in1 = (gt(x, Math.min(this.x1, this.x2)) && lt(x, Math.max(this.x1, this.x2)) &&
+                gt(y, Math.min(this.y1, this.y2)) && lt(y, Math.max(this.y1, this.y2))),
+            in2 = (gt(x, Math.min(that.x1, that.x2)) && lt(x, Math.max(that.x1, that.x2)) &&
+                gt(y, Math.min(that.y1, that.y2)) && lt(y, Math.max(that.y1, that.y2)));
+
+        return {
+            x   : x,
+            y   : y,
+            in1 : in1,
+            in2 : in2
+        };
+    };
+
+    geo.LineSegment.prototype.x = function(y) {
+        // x = (C - By) / a;
+        if (this.a) {
+            return (this.c - this.b * y) / this.a;
+        } else {
+            // a == 0 -> horizontal line
+            return NaN;
+        }
+    };
+
+    geo.LineSegment.prototype.y = function(x) {
+        // y = (C - Ax) / b;
+        if (this.b) {
+            return (this.c - this.a * x) / this.b;
+        } else {
+            // b == 0 -> vertical line
+            return NaN;
+        }
+    };
+
+    geo.LineSegment.prototype.length = function() {
+        return Math.sqrt(
+                (this.y2 - this.y1) * (this.y2 - this.y1) +
+                (this.x2 - this.x1) * (this.x2 - this.x1));
+    };
+
+    geo.LineSegment.prototype.offset = function(x, y) {
+        return new geo.LineSegment(
+                this.x1 + x, this.y1 + y,
+                this.x2 + x, this.y2 + y);
+    };
+
+})();
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 3b0d290..ebf25c5 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -14,6 +14,7 @@
     <link rel="stylesheet" href="base.css">
     <link rel="stylesheet" href="onos.css">
 
+    <script src="geometry.js"></script>
     <script src="onosui.js"></script>
 
 </head>
@@ -32,20 +33,20 @@
         <div id="view"></div>
     </div>
 
-    // Initialize the UI...
+    <!-- Initialize the UI...-->
     <script type="text/javascript">
         var ONOS = $.onos({note: "config, if needed"});
     </script>
 
-    // include module files
-    // + mast.js
-    // + nav.js
-    // + .... application views
+    <!-- include module files-->
+    <!-- + mast.js-->
+    <!-- + nav.js-->
+    <!-- + .... application views-->
 
-    // for now, we are just bootstrapping the network visualization
+    <!-- for now, we are just bootstrapping the network visualization-->
     <script src="network.js" type="text/javascript"></script>
 
-    // finally, build the UI
+    <!-- finally, build the UI-->
     <script type="text/javascript">
         $(ONOS.buildUi);
     </script>
diff --git a/web/gui/src/main/webapp/network.js b/web/gui/src/main/webapp/network.js
index 81105dc..80d11b7 100644
--- a/web/gui/src/main/webapp/network.js
+++ b/web/gui/src/main/webapp/network.js
@@ -10,11 +10,16 @@
     var api = onos.api;
 
     var config = {
+            layering: false,
             jsonUrl: 'network.json',
+            iconUrl: {
+                pkt: 'pkt.png',
+                opt: 'opt.png'
+            },
             mastHeight: 32,
             force: {
-                linkDistance: 150,
-                linkStrength: 0.9,
+                linkDistance: 240,
+                linkStrength: 0.8,
                 charge: -400,
                 ticksWithoutCollisions: 50,
                 marginLR: 20,
@@ -26,8 +31,9 @@
                 }
             },
             labels: {
-                padLR: 3,
-                padTB: 2,
+                imgPad: 22,
+                padLR: 8,
+                padTB: 6,
                 marginLR: 3,
                 marginTB: 2
             },
@@ -53,7 +59,7 @@
         d3.json(config.jsonUrl, function (err, data) {
             if (err) {
                 alert('Oops! Error reading JSON...\n\n' +
-                    'URL: ' + jsonUrl + '\n\n' +
+                    'URL: ' + config.jsonUrl + '\n\n' +
                     'Error: ' + err.message);
                 return;
             }
@@ -100,7 +106,7 @@
 
         network.data.nodes.forEach(function(n) {
             var ypc = yPosConstraintForNode(n),
-                ix = Math.random() * 0.8 * nw + 0.1 * nw,
+                ix = Math.random() * 0.6 * nw + 0.2 * nw,
                 iy = ypc * nh,
                 node = {
                     id: n.id,
@@ -153,10 +159,48 @@
             .attr('height', view.height)
             .append('g')
             .attr('transform', config.force.translate());
+//            .attr('id', 'zoomable')
+//            .call(d3.behavior.zoom().on("zoom", zoomRedraw));
 
-        // TODO: svg.append('defs')
-        // TODO: glow/blur stuff
+//        function zoomRedraw() {
+//            d3.select("#zoomable").attr("transform",
+//                    "translate(" + d3.event.translate + ")"
+//                    + " scale(" + d3.event.scale + ")");
+//        }
+
+        // TODO: svg.append('defs') for markers?
+
+        // TODO: move glow/blur stuff to util script
+        var glow = network.svg.append('filter')
+            .attr('x', '-50%')
+            .attr('y', '-50%')
+            .attr('width', '200%')
+            .attr('height', '200%')
+            .attr('id', 'blue-glow');
+
+        glow.append('feColorMatrix')
+            .attr('type', 'matrix')
+            .attr('values', '0 0 0 0  0 ' +
+                '0 0 0 0  0 ' +
+                '0 0 0 0  .7 ' +
+                '0 0 0 1  0 ');
+
+        glow.append('feGaussianBlur')
+            .attr('stdDeviation', 3)
+            .attr('result', 'coloredBlur');
+
+        glow.append('feMerge').selectAll('feMergeNode')
+            .data(['coloredBlur', 'SourceGraphic'])
+            .enter().append('feMergeNode')
+            .attr('in', String);
+
         // TODO: legend (and auto adjust on scroll)
+//        $('#view').on('scroll', function() {
+//
+//        });
+
+
+
 
         network.link = network.svg.append('g').selectAll('.link')
             .data(network.force.links(), function(d) {return d.id})
@@ -164,37 +208,103 @@
             .attr('class', 'link');
 
         // TODO: drag behavior
-        // TODO: closest node deselect
+        network.draggedThreshold = d3.scale.linear()
+            .domain([0, 0.1])
+            .range([5, 20])
+            .clamp(true);
 
-        // TODO: add drag, mouseover, mouseout behaviors
+        function dragged(d) {
+            var threshold = network.draggedThreshold(network.force.alpha()),
+                dx = d.oldX - d.px,
+                dy = d.oldY - d.py;
+            if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+                d.dragged = true;
+            }
+            return d.dragged;
+        }
+
+        network.drag = d3.behavior.drag()
+            .origin(function(d) { return d; })
+            .on('dragstart', function(d) {
+                d.oldX = d.x;
+                d.oldY = d.y;
+                d.dragged = false;
+                d.fixed |= 2;
+            })
+            .on('drag', function(d) {
+                d.px = d3.event.x;
+                d.py = d3.event.y;
+                if (dragged(d)) {
+                    if (!network.force.alpha()) {
+                        network.force.alpha(.025);
+                    }
+                }
+            })
+            .on('dragend', function(d) {
+                if (!dragged(d)) {
+                    selectObject(d, this);
+                }
+                d.fixed &= ~6;
+            });
+
+        $('#view').on('click', function(e) {
+            if (!$(e.target).closest('.node').length) {
+                deselectObject();
+            }
+        });
+
+
         network.node = network.svg.selectAll('.node')
             .data(network.force.nodes(), function(d) {return d.id})
             .enter().append('g')
-            .attr('class', 'node')
+            .attr('class', function(d) {
+                return 'node ' + d.type;
+            })
             .attr('transform', function(d) {
                 return translate(d.x, d.y);
             })
-        //        .call(network.drag)
-            .on('mouseover', function(d) {})
-            .on('mouseout', function(d) {});
+            .call(network.drag)
+            .on('mouseover', function(d) {
+                if (!selected.obj) {
+                    if (network.mouseoutTimeout) {
+                        clearTimeout(network.mouseoutTimeout);
+                        network.mouseoutTimeout = null;
+                    }
+                    highlightObject(d);
+                }
+            })
+            .on('mouseout', function(d) {
+                if (!selected.obj) {
+                    if (network.mouseoutTimeout) {
+                        clearTimeout(network.mouseoutTimeout);
+                        network.mouseoutTimeout = null;
+                    }
+                    network.mouseoutTimeout = setTimeout(function() {
+                        highlightObject(null);
+                    }, 160);
+                }
+            });
 
-        // TODO: augment stroke and fill functions
         network.nodeRect = network.node.append('rect')
-            // TODO: css for node rects
             .attr('rx', 5)
             .attr('ry', 5)
-            .attr('stroke', function(d) { return '#000'})
-            .attr('fill', function(d) { return '#ddf'})
-            .attr('width', 60)
-            .attr('height', 24);
+            .attr('width', 126)
+            .attr('height', 40);
 
         network.node.each(function(d) {
             var node = d3.select(this),
-                rect = node.select('rect');
-            var text = node.append('text')
-                .text(d.id)
-                .attr('dx', '1em')
-                .attr('dy', '2.1em');
+                rect = node.select('rect'),
+                img = node.append('svg:image')
+                    .attr('x', -16)
+                    .attr('y', -16)
+                    .attr('width', 32)
+                    .attr('height', 32)
+                    .attr('xlink:href', iconUrl(d)),
+                text = node.append('text')
+                    .text(d.id)
+                    .attr('dy', '1.1em'),
+                dummy;
+
         });
 
         // this function is scheduled to happen soon after the given thread ends
@@ -207,12 +317,64 @@
                     first = true;
 
                 // NOTE: probably unnecessary code if we only have one line.
+                text.each(function() {
+                    var box = this.getBBox();
+                    if (first || box.x < bounds.x1) {
+                        bounds.x1 = box.x;
+                    }
+                    if (first || box.y < bounds.y1) {
+                        bounds.y1 = box.y;
+                    }
+                    if (first || box.x + box.width < bounds.x2) {
+                        bounds.x2 = box.x + box.width;
+                    }
+                    if (first || box.y + box.height < bounds.y2) {
+                        bounds.y2 = box.y + box.height;
+                    }
+                    first = false;
+                }).attr('text-anchor', 'middle');
+
+                var lab = config.labels,
+                    oldWidth = bounds.x2 - bounds.x1;
+
+                bounds.x1 -= oldWidth / 2;
+                bounds.x2 -= oldWidth / 2;
+
+                bounds.x1 -= (lab.padLR + lab.imgPad);
+                bounds.y1 -= lab.padTB;
+                bounds.x2 += lab.padLR;
+                bounds.y2 += lab.padTB;
+
+                node.select('rect')
+                    .attr('x', bounds.x1)
+                    .attr('y', bounds.y1)
+                    .attr('width', bounds.x2 - bounds.x1)
+                    .attr('height', bounds.y2 - bounds.y1);
+
+                node.select('image')
+                    .attr('x', bounds.x1);
+
+                d.extent = {
+                    left: bounds.x1 - lab.marginLR,
+                    right: bounds.x2 + lab.marginLR,
+                    top: bounds.y1 - lab.marginTB,
+                    bottom: bounds.y2 + lab.marginTB
+                };
+
+                d.edge = {
+                    left   : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
+                    right  : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
+                    top    : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
+                    bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
+                };
+
+                // ====
             });
 
             network.numTicks = 0;
             network.preventCollisions = false;
             network.force.start();
-            for (var i = 0; i < config.ticksWithoutCollisions; i++) {
+            for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
                 network.force.tick();
             }
             network.preventCollisions = true;
@@ -221,22 +383,78 @@
 
     }
 
+    function iconUrl(d) {
+        return config.iconUrl[d.type];
+    }
+
     function translate(x, y) {
         return 'translate(' + x + ',' + y + ')';
     }
 
+    function preventCollisions() {
+        var quadtree = d3.geom.quadtree(network.nodes);
+
+        network.nodes.forEach(function(n) {
+            var nx1 = n.x + n.extent.left,
+                nx2 = n.x + n.extent.right,
+                ny1 = n.y + n.extent.top,
+                ny2 = n.y + n.extent.bottom;
+
+            quadtree.visit(function(quad, x1, y1, x2, y2) {
+                if (quad.point && quad.point !== n) {
+                    // check if the rectangles intersect
+                    var p = quad.point,
+                        px1 = p.x + p.extent.left,
+                        px2 = p.x + p.extent.right,
+                        py1 = p.y + p.extent.top,
+                        py2 = p.y + p.extent.bottom,
+                        ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
+                    if (ix) {
+                        var xa1 = nx2 - px1, // shift n left , p right
+                            xa2 = px2 - nx1, // shift n right, p left
+                            ya1 = ny2 - py1, // shift n up   , p down
+                            ya2 = py2 - ny1, // shift n down , p up
+                            adj = Math.min(xa1, xa2, ya1, ya2);
+
+                        if (adj == xa1) {
+                            n.x -= adj / 2;
+                            p.x += adj / 2;
+                        } else if (adj == xa2) {
+                            n.x += adj / 2;
+                            p.x -= adj / 2;
+                        } else if (adj == ya1) {
+                            n.y -= adj / 2;
+                            p.y += adj / 2;
+                        } else if (adj == ya2) {
+                            n.y += adj / 2;
+                            p.y -= adj / 2;
+                        }
+                    }
+                    return ix;
+                }
+            });
+
+        });
+    }
 
     function tick(e) {
         network.numTicks++;
 
-        // adjust the y-coord of each node, based on y-pos constraints
-//        network.nodes.forEach(function (n) {
-//            var z = e.alpha * n.constraint.weight;
-//            if (!isNaN(n.constraint.y)) {
-//                n.y = (n.constraint.y * z + n.y * (1 - z));
-//            }
-//        });
+        if (config.layering) {
+            // adjust the y-coord of each node, based on y-pos constraints
+            network.nodes.forEach(function (n) {
+                var z = e.alpha * n.constraint.weight;
+                if (!isNaN(n.constraint.y)) {
+                    n.y = (n.constraint.y * z + n.y * (1 - z));
+                }
+            });
+        }
 
+        if (network.preventCollisions) {
+            preventCollisions();
+        }
+
+        // TODO: use intersection technique for source end of link also
         network.link
             .attr('x1', function(d) {
                 return d.source.x;
@@ -244,11 +462,24 @@
             .attr('y1', function(d) {
                 return d.source.y;
             })
-            .attr('x2', function(d) {
-                return d.target.x;
-            })
-            .attr('y2', function(d) {
-                return d.target.y;
+            .each(function(d) {
+                var x = d.target.x,
+                    y = d.target.y,
+                    line = new geo.LineSegment(d.source.x, d.source.y, x, y);
+
+                for (var e in d.target.edge) {
+                    var ix = line.intersect(d.target.edge[e].offset(x,y));
+                    if (ix.in1 && ix.in2) {
+                        x = ix.x;
+                        y = ix.y;
+                        break;
+                    }
+                }
+
+                d3.select(this)
+                    .attr('x2', x)
+                    .attr('y2', y);
+
             });
 
         network.node
diff --git a/web/gui/src/main/webapp/network.json b/web/gui/src/main/webapp/network.json
index 74a276a..b4f1b6e 100644
--- a/web/gui/src/main/webapp/network.json
+++ b/web/gui/src/main/webapp/network.json
@@ -7,50 +7,50 @@
     },
     "nodes": [
         {
-            "id": "switch-1",
+            "id": "sample1",
             "type": "opt",
             "status": "good"
         },
         {
-            "id": "switch-2",
+            "id": "00:00:00:00:00:00:00:02",
             "type": "opt",
             "status": "good"
         },
         {
-            "id": "switch-3",
+            "id": "00:00:00:00:00:00:00:03",
             "type": "opt",
             "status": "good"
         },
         {
-            "id": "switch-4",
+            "id": "00:00:00:00:00:00:00:04",
             "type": "opt",
             "status": "good"
         },
         {
-            "id": "switch-11",
+            "id": "00:00:00:00:00:00:00:11",
             "type": "pkt",
             "status": "good"
         },
         {
-            "id": "switch-12",
+            "id": "00:00:00:00:00:00:00:12",
             "type": "pkt",
             "status": "good"
         },
         {
-            "id": "switch-13",
+            "id": "00:00:00:00:00:00:00:13",
             "type": "pkt",
             "status": "good"
         }
     ],
     "links": [
-        { "src": "switch-1", "dst": "switch-2" },
-        { "src": "switch-1", "dst": "switch-3" },
-        { "src": "switch-1", "dst": "switch-4" },
-        { "src": "switch-2", "dst": "switch-3" },
-        { "src": "switch-2", "dst": "switch-4" },
-        { "src": "switch-3", "dst": "switch-4" },
-        { "src": "switch-13", "dst": "switch-3" },
-        { "src": "switch-12", "dst": "switch-2" },
-        { "src": "switch-11", "dst": "switch-1" }
+        { "src": "sample1", "dst": "00:00:00:00:00:00:00:02" },
+        { "src": "sample1", "dst": "00:00:00:00:00:00:00:03" },
+        { "src": "sample1", "dst": "00:00:00:00:00:00:00:04" },
+        { "src": "00:00:00:00:00:00:00:02", "dst": "00:00:00:00:00:00:00:03" },
+        { "src": "00:00:00:00:00:00:00:02", "dst": "00:00:00:00:00:00:00:04" },
+        { "src": "00:00:00:00:00:00:00:03", "dst": "00:00:00:00:00:00:00:04" },
+        { "src": "00:00:00:00:00:00:00:13", "dst": "00:00:00:00:00:00:00:03" },
+        { "src": "00:00:00:00:00:00:00:12", "dst": "00:00:00:00:00:00:00:02" },
+        { "src": "00:00:00:00:00:00:00:11", "dst": "sample1" }
     ]
 }
diff --git a/web/gui/src/main/webapp/onos.css b/web/gui/src/main/webapp/onos.css
index 328e109..340ae79 100644
--- a/web/gui/src/main/webapp/onos.css
+++ b/web/gui/src/main/webapp/onos.css
@@ -13,7 +13,7 @@
  */
 
 span.title {
-    color: red;
+    color: darkblue;
     font-size: 16pt;
     font-style: italic;
 }
@@ -30,7 +30,7 @@
  * === DEBUGGING ======
  */
 svg {
-    border: 1px dashed red;
+    /*border: 1px dashed red;*/
 }
 
 
@@ -64,36 +64,47 @@
     -moz-transition: opacity 250ms;
 }
 
-.node text {
-    fill: #000;
+/*differentiate between packet and optical nodes*/
+svg .node.pkt rect {
+    fill: #77a;
+}
+
+svg .node.opt rect {
+    fill: #7a7;
+}
+
+svg .node text {
+    fill: white;
     font: 10px sans-serif;
     pointer-events: none;
 }
 
-.node.selected rect {
+svg .node.selected rect {
     filter: url(#blue-glow);
 }
 
-.link.inactive,
-.node.inactive rect,
-.node.inactive text {
+svg .link.inactive,
+svg .node.inactive rect,
+svg .node.inactive text,
+svg .node.inactive image {
     opacity: .2;
 }
 
-.node.inactive.selected rect,
-.node.inactive.selected text {
+svg .node.inactive.selected rect,
+svg .node.inactive.selected text,
+svg .node.inactive.selected image {
     opacity: .6;
 }
 
-.legend {
+svg .legend {
     position: fixed;
 }
 
-.legend .category rect {
+svg .legend .category rect {
     stroke-width: 1px;
 }
 
-.legend .category text {
+svg .legend .category text {
     fill: #000;
     font: 10px sans-serif;
     pointer-events: none;
@@ -110,15 +121,15 @@
 #frame {
     width: 100%;
     height: 100%;
-    background-color: #ffd;
+    background-color: #cdf;
 }
 
 #mast {
     height: 32px;
-    background-color: #dda;
+    background-color: #abe;
     vertical-align: baseline;
 }
 
 #main {
-    background-color: #99b;
+    background-color: #99c;
 }
diff --git a/web/gui/src/main/webapp/opt.png b/web/gui/src/main/webapp/opt.png
new file mode 100644
index 0000000..2f2c88e
--- /dev/null
+++ b/web/gui/src/main/webapp/opt.png
Binary files differ
diff --git a/web/gui/src/main/webapp/pkt.png b/web/gui/src/main/webapp/pkt.png
new file mode 100644
index 0000000..1b1d5a3
--- /dev/null
+++ b/web/gui/src/main/webapp/pkt.png
Binary files differ
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>