Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/.gitignore b/.gitignore
index 0b28617..8f725d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,6 @@
 .checkstyle
 target
 *.iml
+*.pyc
 dependency-reduced-pom.xml
 .idea
diff --git a/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
index cfa1a63..f6a0f04 100644
--- a/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
+++ b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
@@ -16,14 +16,24 @@
 package org.onlab.onos.calendar;
 
 import java.net.URI;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentEvent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentListener;
 import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.net.intent.IntentState;
 import org.onlab.rest.BaseResource;
+
 import javax.ws.rs.POST;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.core.Response;
+
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.core.CoreService;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
@@ -31,10 +41,15 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.intent.PointToPointIntent;
 import org.onlab.packet.Ethernet;
+
 import static org.onlab.onos.net.PortNumber.portNumber;
 import static org.onlab.onos.net.flow.DefaultTrafficTreatment.builder;
 
+import static org.onlab.onos.net.intent.IntentState.FAILED;
+import static org.onlab.onos.net.intent.IntentState.INSTALLED;
+import static org.onlab.onos.net.intent.IntentState.WITHDRAWN;
 import static org.slf4j.LoggerFactory.getLogger;
+
 import org.slf4j.Logger;
 
 /**
@@ -44,6 +59,7 @@
 public class BandwidthCalendarResource extends BaseResource {
 
     private static final Logger log = getLogger(BandwidthCalendarResource.class);
+    private static final long TIMEOUT = 5; // seconds
 
     @javax.ws.rs.Path("/{src}/{dst}/{srcPort}/{dstPort}/{bandwidth}")
     @POST
@@ -55,7 +71,7 @@
 
         log.info("Receiving Create Intent request...");
         log.info("Path Constraints: Src = {} SrcPort = {} Dest = {} DestPort = {} BW = {}",
-                src, srcPort, dst, dstPort, bandwidth);
+                 src, srcPort, dst, dstPort, bandwidth);
 
         IntentService service = get(IntentService.class);
 
@@ -66,36 +82,50 @@
         TrafficTreatment treatment = builder().build();
 
         PointToPointIntent intentP2P =
-                        new PointToPointIntent(appId(), selector, treatment,
-                                               srcPoint, dstPoint);
-        service.submit(intentP2P);
-        log.info("Submitted Calendar App intent: src = " + src + "dest = " + dst
-                + "srcPort = " + srcPort + "destPort" + dstPort + "intentID = " + intentP2P.id().toString());
-        String reply =  intentP2P.id().toString() + "\n";
+                new PointToPointIntent(appId(), selector, treatment,
+                                       srcPoint, dstPoint);
 
-        return Response.ok(reply).build();
+        CountDownLatch latch = new CountDownLatch(1);
+        InternalIntentListener listener = new InternalIntentListener(intentP2P, service, latch);
+        service.addListener(listener);
+        service.submit(intentP2P);
+        try {
+            if (latch.await(TIMEOUT, TimeUnit.SECONDS)) {
+                log.info("Submitted Calendar App intent: src = {}; dst = {}; " +
+                                 "srcPort = {}; dstPort = {}; intentID = {}",
+                         src, dst, srcPort, dstPort, intentP2P.id());
+                String reply = intentP2P.id() + " " + listener.getState() + "\n";
+                return Response.ok(reply).build();
+            }
+        } catch (InterruptedException e) {
+            log.warn("Interrupted while waiting for intent {} status", intentP2P.id());
+        }
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
     }
 
     @javax.ws.rs.Path("/cancellation/{intentId}")
     @DELETE
     public Response withdrawIntent(@PathParam("intentId") String intentId) {
-
-        log.info("Receiving Teardown request...");
-        log.info("Withdraw intentId = {} ", intentId);
-
-        String reply =  "ok\n";
-        return Response.ok(reply).build();
+        log.info("Receiving Teardown request for {}", intentId);
+        IntentService service = get(IntentService.class);
+        Intent intent = service.getIntent(IntentId.valueOf(Long.parseLong(intentId)));
+        if (intent != null) {
+            service.withdraw(intent);
+            String reply = "ok\n";
+            return Response.ok(reply).build();
+        }
+        return Response.status(Response.Status.NOT_FOUND).build();
     }
 
     @javax.ws.rs.Path("/modification/{intentId}/{bandwidth}")
     @POST
     public Response modifyBandwidth(@PathParam("intentId") String intentId,
-                                 @PathParam("bandwidth") String bandwidth) {
+                                    @PathParam("bandwidth") String bandwidth) {
 
         log.info("Receiving Modify request...");
         log.info("Modify bw for intentId = {} with new bandwidth = {}", intentId, bandwidth);
 
-        String reply =  "ok\n";
+        String reply = "ok\n";
         return Response.ok(reply).build();
     }
 
@@ -115,4 +145,34 @@
     protected ApplicationId appId() {
         return get(CoreService.class).registerApplication("org.onlab.onos.calendar");
     }
+
+    // Auxiliary listener to wait until the given intent reaches the installed or failed states.
+    private final class InternalIntentListener implements IntentListener {
+        private final Intent intent;
+        private final IntentService service;
+        private final CountDownLatch latch;
+        private IntentState state;
+
+        private InternalIntentListener(Intent intent, IntentService service,
+                                       CountDownLatch latch) {
+            this.intent = intent;
+            this.service = service;
+            this.latch = latch;
+        }
+
+        @Override
+        public void event(IntentEvent event) {
+            if (event.subject().equals(intent)) {
+                state = service.getIntentState(intent.id());
+                if (state == INSTALLED || state == FAILED || state == WITHDRAWN) {
+                    latch.countDown();
+                }
+                service.removeListener(this);
+            }
+        }
+
+        public IntentState getState() {
+            return state;
+        }
+    }
 }
diff --git a/apps/config/src/main/java/org/onlab/onos/config/NetworkConfigReader.java b/apps/config/src/main/java/org/onlab/onos/config/NetworkConfigReader.java
index f015e36..846e83c 100644
--- a/apps/config/src/main/java/org/onlab/onos/config/NetworkConfigReader.java
+++ b/apps/config/src/main/java/org/onlab/onos/config/NetworkConfigReader.java
@@ -50,7 +50,10 @@
 
     private final Logger log = getLogger(getClass());
 
-    private static final String DEFAULT_CONFIG_FILE = "config/addresses.json";
+    // Current working dir seems to be /opt/onos/apache-karaf-3.0.2
+    // TODO: Set the path to /opt/onos/config
+    private static final String CONFIG_DIR = "../config";
+    private static final String DEFAULT_CONFIG_FILE = "addresses.json";
     private String configFileName = DEFAULT_CONFIG_FILE;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -60,52 +63,9 @@
     protected void activate() {
         log.info("Started network config reader");
 
-        log.info("Config file set to {}", configFileName);
-
         AddressConfiguration config = readNetworkConfig();
-
         if (config != null) {
-            for (AddressEntry entry : config.getAddresses()) {
-
-                ConnectPoint cp = new ConnectPoint(
-                        DeviceId.deviceId(dpidToUri(entry.getDpid())),
-                        PortNumber.portNumber(entry.getPortNumber()));
-
-                Set<InterfaceIpAddress> interfaceIpAddresses = new HashSet<>();
-
-                for (String strIp : entry.getIpAddresses()) {
-                    // Get the IP address and the subnet mask length
-                    try {
-                        String[] splits = strIp.split("/");
-                        if (splits.length != 2) {
-                            throw new IllegalArgumentException("Invalid IP address and prefix length format");
-                        }
-                        // NOTE: IpPrefix will mask-out the bits after the prefix length.
-                        IpPrefix subnet = IpPrefix.valueOf(strIp);
-                        IpAddress addr = IpAddress.valueOf(splits[0]);
-                        InterfaceIpAddress ia =
-                            new InterfaceIpAddress(addr, subnet);
-                        interfaceIpAddresses.add(ia);
-                    } catch (IllegalArgumentException e) {
-                        log.warn("Bad format for IP address in config: {}", strIp);
-                    }
-                }
-
-                MacAddress macAddress = null;
-                if (entry.getMacAddress() != null) {
-                    try {
-                        macAddress = MacAddress.valueOf(entry.getMacAddress());
-                    } catch (IllegalArgumentException e) {
-                        log.warn("Bad format for MAC address in config: {}",
-                                entry.getMacAddress());
-                    }
-                }
-
-                PortAddresses addresses = new PortAddresses(cp,
-                        interfaceIpAddresses, macAddress);
-
-                hostAdminService.bindAddressesToPort(addresses);
-            }
+            applyNetworkConfig(config);
         }
     }
 
@@ -114,12 +74,17 @@
         log.info("Stopped");
     }
 
+    /**
+     * Reads the network configuration.
+     *
+     * @return the network configuration on success, otherwise null
+     */
     private AddressConfiguration readNetworkConfig() {
-        File configFile = new File(configFileName);
-
+        File configFile = new File(CONFIG_DIR, configFileName);
         ObjectMapper mapper = new ObjectMapper();
 
         try {
+            log.info("Loading config: {}", configFile.getAbsolutePath());
             AddressConfiguration config =
                     mapper.readValue(configFile, AddressConfiguration.class);
 
@@ -127,12 +92,58 @@
         } catch (FileNotFoundException e) {
             log.warn("Configuration file not found: {}", configFileName);
         } catch (IOException e) {
-            log.error("Unable to read config from file:", e);
+            log.error("Error loading configuration", e);
         }
 
         return null;
     }
 
+    /**
+     * Applies the network configuration.
+     *
+     * @param config the network configuration to apply
+     */
+    private void applyNetworkConfig(AddressConfiguration config) {
+        for (AddressEntry entry : config.getAddresses()) {
+            ConnectPoint cp = new ConnectPoint(
+                        DeviceId.deviceId(dpidToUri(entry.getDpid())),
+                        PortNumber.portNumber(entry.getPortNumber()));
+
+            Set<InterfaceIpAddress> interfaceIpAddresses = new HashSet<>();
+            for (String strIp : entry.getIpAddresses()) {
+                // Get the IP address and the subnet mask length
+                try {
+                    String[] splits = strIp.split("/");
+                    if (splits.length != 2) {
+                        throw new IllegalArgumentException("Invalid IP address and prefix length format");
+                    }
+                    // NOTE: IpPrefix will mask-out the bits after the prefix length.
+                    IpPrefix subnet = IpPrefix.valueOf(strIp);
+                    IpAddress addr = IpAddress.valueOf(splits[0]);
+                    InterfaceIpAddress ia =
+                        new InterfaceIpAddress(addr, subnet);
+                    interfaceIpAddresses.add(ia);
+                } catch (IllegalArgumentException e) {
+                    log.warn("Bad format for IP address in config: {}", strIp);
+                }
+            }
+
+            MacAddress macAddress = null;
+            if (entry.getMacAddress() != null) {
+                try {
+                    macAddress = MacAddress.valueOf(entry.getMacAddress());
+                } catch (IllegalArgumentException e) {
+                    log.warn("Bad format for MAC address in config: {}",
+                             entry.getMacAddress());
+                }
+            }
+
+            PortAddresses addresses = new PortAddresses(cp,
+                        interfaceIpAddresses, macAddress);
+            hostAdminService.bindAddressesToPort(addresses);
+        }
+    }
+
     private static String dpidToUri(String dpid) {
         return "of:" + dpid.replace(":", "");
     }
diff --git a/apps/sdnip/src/main/resources/config-examples/addresses.json b/apps/config/src/main/resources/addresses.json
similarity index 100%
rename from apps/sdnip/src/main/resources/config-examples/addresses.json
rename to apps/config/src/main/resources/addresses.json
diff --git a/apps/config/src/main/resources/config.json b/apps/config/src/main/resources/config.json
deleted file mode 100644
index ca4be83..0000000
--- a/apps/config/src/main/resources/config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-    "interfaces" : [
-	{
-	    "dpid" : "00:00:00:00:00:00:01",
-	    "port" : "1",
-	    "ips" : ["192.168.10.101/24"],
-	    "mac" : "00:00:00:11:22:33"
-	},
-	{
-	    "dpid" : "00:00:00:00:00:00:02",
-	    "port" : "1",
-	    "ips" : ["192.168.20.101/24", "192.168.30.101/24"]
-	},
-	{
-	    "dpid" : "00:00:00:00:00:00:03",
-	    "port" : "1",
-	    "ips" : ["10.1.0.1/16"],
-	    "mac" : "00:00:00:00:00:01"
-	}
-    ]
-}
diff --git a/apps/demo/pom.xml b/apps/demo/pom.xml
new file mode 100644
index 0000000..fbc7f91
--- /dev/null
+++ b/apps/demo/pom.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2014 Open Networking Laboratory
+  ~
+  ~ Licensed 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.
+  -->
+<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-apps</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-app-demo</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS demo app bundle</description>
+
+    <properties>
+        <web.context>/onos/demo</web.context>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onos-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>1.18.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-grizzly2</artifactId>
+            <version>1.18.1</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <_wab>src/main/webapp/</_wab>
+                        <Bundle-SymbolicName>
+                            ${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,
+                            com.sun.jersey.spi.container.servlet,
+                            com.sun.jersey.server.impl.container.servlet,
+                            com.fasterxml.jackson.databind,
+                            com.fasterxml.jackson.databind.node,
+                            com.google.common.*,
+                            org.onlab.packet.*,
+                            org.onlab.rest.*,
+                            org.onlab.onos.*
+                        </Import-Package>
+                        <Web-ContextPath>${web.context}</Web-ContextPath>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/apps/demo/src/main/java/org/onlab/onos/demo/DemoAPI.java b/apps/demo/src/main/java/org/onlab/onos/demo/DemoAPI.java
new file mode 100644
index 0000000..2a267f3
--- /dev/null
+++ b/apps/demo/src/main/java/org/onlab/onos/demo/DemoAPI.java
@@ -0,0 +1,21 @@
+package org.onlab.onos.demo;
+
+/**
+ * Simple demo api interface.
+ */
+public interface DemoAPI {
+
+    enum InstallType { MESH, RANDOM };
+
+    /**
+     * Installs intents based on the installation type.
+     * @param type the installation type.
+     */
+    void setup(InstallType type);
+
+    /**
+     * Uninstalls all existing intents.
+     */
+    void tearDown();
+
+}
diff --git a/apps/demo/src/main/java/org/onlab/onos/demo/DemoInstaller.java b/apps/demo/src/main/java/org/onlab/onos/demo/DemoInstaller.java
new file mode 100644
index 0000000..ca2d6db
--- /dev/null
+++ b/apps/demo/src/main/java/org/onlab/onos/demo/DemoInstaller.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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.demo;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+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.core.ApplicationId;
+import org.onlab.onos.core.CoreService;
+import org.onlab.onos.net.Host;
+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.HostToHostIntent;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentService;
+import org.slf4j.Logger;
+
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Application to set up demos.
+ */
+@Component(immediate = true)
+@Service
+public class DemoInstaller implements DemoAPI {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentService intentService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    private ExecutorService worker;
+
+    private ApplicationId appId;
+
+    private final Set<Intent> existingIntents = new HashSet<>();
+
+
+
+    @Activate
+    public void activate() {
+        appId = coreService.registerApplication("org.onlab.onos.demo.installer");
+        worker = Executors.newFixedThreadPool(1,
+                                              new ThreadFactoryBuilder()
+                                                      .setNameFormat("demo-app-worker")
+                                                      .build());
+        log.info("Started with Application ID {}", appId.id());
+    }
+
+    @Deactivate
+    public void deactivate() {
+        worker.shutdownNow();
+        log.info("Stopped");
+    }
+
+    @Override
+    public void setup(InstallType type) {
+        switch (type) {
+            case MESH:
+                log.debug("Installing mesh intents");
+                worker.execute(new MeshInstaller());
+                break;
+            case RANDOM:
+                throw new IllegalArgumentException("Not yet implemented.");
+            default:
+                throw new IllegalArgumentException("What is it you want exactly?");
+        }
+    }
+
+    @Override
+    public void tearDown() {
+        worker.submit(new UnInstaller());
+    }
+
+
+    private class MeshInstaller implements Runnable {
+
+        @Override
+        public void run() {
+            TrafficSelector selector = DefaultTrafficSelector.builder().build();
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+
+            List<Host> hosts = Lists.newArrayList(hostService.getHosts());
+            while (!hosts.isEmpty()) {
+                Host src = hosts.remove(0);
+                for (Host dst : hosts) {
+                    HostToHostIntent intent = new HostToHostIntent(appId, src.id(), dst.id(),
+                                                                   selector, treatment,
+                                                                   null);
+                    existingIntents.add(intent);
+                    intentService.submit(intent);
+                }
+            }
+        }
+    }
+
+
+    private class UnInstaller implements Runnable {
+        @Override
+        public void run() {
+            for (Intent i : existingIntents) {
+                intentService.withdraw(i);
+            }
+        }
+    }
+}
+
+
diff --git a/apps/demo/src/main/java/org/onlab/onos/demo/DemoResource.java b/apps/demo/src/main/java/org/onlab/onos/demo/DemoResource.java
new file mode 100644
index 0000000..f533072
--- /dev/null
+++ b/apps/demo/src/main/java/org/onlab/onos/demo/DemoResource.java
@@ -0,0 +1,54 @@
+package org.onlab.onos.demo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.onlab.rest.BaseResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+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;
+
+/**
+ * Rest API for demos.
+ */
+@Path("intents")
+public class DemoResource extends BaseResource {
+
+
+    @POST
+    @Path("setup")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response setup(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode cfg = mapper.readTree(input);
+        if (!cfg.has("type")) {
+            return Response.status(Response.Status.BAD_REQUEST)
+                    .entity("Expected type field containing either mesh or random.").build();
+        }
+
+        DemoAPI.InstallType type = DemoAPI.InstallType.valueOf(
+                cfg.get("type").asText().toUpperCase());
+        DemoAPI demo = get(DemoAPI.class);
+        demo.setup(type);
+
+        return Response.ok(mapper.createObjectNode().toString()).build();
+    }
+
+    @GET
+    @Path("teardown")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response tearDown() throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        DemoAPI demo = get(DemoAPI.class);
+        demo.tearDown();
+        return Response.ok(mapper.createObjectNode().toString()).build();
+    }
+
+}
diff --git a/apps/demo/src/main/java/org/onlab/onos/demo/package-info.java b/apps/demo/src/main/java/org/onlab/onos/demo/package-info.java
new file mode 100644
index 0000000..b60eb15
--- /dev/null
+++ b/apps/demo/src/main/java/org/onlab/onos/demo/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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.
+ */
+
+/**
+ *  Demo applications live here.
+ */
+package org.onlab.onos.demo;
diff --git a/apps/demo/src/main/webapp/WEB-INF/web.xml b/apps/demo/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..429287e
--- /dev/null
+++ b/apps/demo/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2014 Open Networking Laboratory
+  ~
+  ~ Licensed 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.
+  -->
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="ONOS" version="2.5">
+    <display-name>ONOS DEMO APP API v1.0</display-name>
+
+    <servlet>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
+            <param-value>com.sun.jersey.api.core.ClassNamesResourceConfig</param-value>
+        </init-param>
+        <init-param>
+            <param-name>com.sun.jersey.config.property.classnames</param-name>
+            <param-value>
+                org.onlab.onos.demo.DemoResource
+            </param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+</web-app>
\ No newline at end of file
diff --git a/apps/pom.xml b/apps/pom.xml
index 3084916..c021ca4 100644
--- a/apps/pom.xml
+++ b/apps/pom.xml
@@ -44,6 +44,7 @@
         <module>optical</module>
         <module>metrics</module>
         <module>oecfg</module>
+        <module>demo</module>
     </modules>
 
     <properties>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
index 2fcd1fe..7262c42 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
@@ -37,24 +37,31 @@
  */
 public class SdnIpConfigReader implements SdnIpConfigService {
 
-    private static final Logger log = LoggerFactory.getLogger(SdnIpConfigReader.class);
+    private final Logger log = LoggerFactory.getLogger(getClass());
 
-    private static final String DEFAULT_CONFIG_FILE = "config/sdnip.json";
+    // Current working dir seems to be /opt/onos/apache-karaf-3.0.2
+    // TODO: Set the path to /opt/onos/config
+    private static final String CONFIG_DIR = "../config";
+    private static final String DEFAULT_CONFIG_FILE = "sdnip.json";
     private String configFileName = DEFAULT_CONFIG_FILE;
+
     private Map<String, BgpSpeaker> bgpSpeakers = new ConcurrentHashMap<>();
     private Map<IpAddress, BgpPeer> bgpPeers = new ConcurrentHashMap<>();
 
     /**
-     * Reads the info contained in the configuration file.
+     * Reads SDN-IP related information contained in the configuration file.
      *
-     * @param configFilename The name of configuration file for SDN-IP application.
+     * @param configFilename the name of the configuration file for the SDN-IP
+     * application
      */
     private void readConfiguration(String configFilename) {
-        File gatewaysFile = new File(configFilename);
+        File configFile = new File(CONFIG_DIR, configFilename);
         ObjectMapper mapper = new ObjectMapper();
 
         try {
-            Configuration config = mapper.readValue(gatewaysFile, Configuration.class);
+            log.info("Loading config: {}", configFile.getAbsolutePath());
+            Configuration config = mapper.readValue(configFile,
+                                                    Configuration.class);
             for (BgpSpeaker speaker : config.getBgpSpeakers()) {
                 bgpSpeakers.put(speaker.name(), speaker);
             }
@@ -64,13 +71,11 @@
         } catch (FileNotFoundException e) {
             log.warn("Configuration file not found: {}", configFileName);
         } catch (IOException e) {
-            log.error("Error reading JSON file", e);
+            log.error("Error loading configuration", e);
         }
     }
 
     public void init() {
-        log.debug("Config file set to {}", configFileName);
-
         readConfiguration(configFileName);
     }
 
diff --git a/apps/sdnip/src/main/resources/config-examples/README b/apps/sdnip/src/main/resources/config-examples/README
index 502285e..7642a4d 100644
--- a/apps/sdnip/src/main/resources/config-examples/README
+++ b/apps/sdnip/src/main/resources/config-examples/README
@@ -1 +1,5 @@
-ONOS looks for these config files by default in $KARAF_LOG/config/
\ No newline at end of file
+The SDN-IP configuration files should be copied to directory
+  $ONOS_HOME/tools/package/config
+
+After deployment and starting up the ONOS cluster, ONOS looks for these
+configuration files in /opt/onos/config on each cluster member.
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
index e4fc5aa..5c513e3 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
@@ -27,6 +27,7 @@
 import org.onlab.onos.net.intent.constraint.LambdaConstraint;
 import org.onlab.onos.net.resource.Bandwidth;
 import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
@@ -48,6 +49,26 @@
             required = false, multiValued = false)
     private String ethTypeString = "";
 
+    @Option(name = "--ipProto", description = "IP Protocol",
+            required = false, multiValued = false)
+    private String ipProtoString = null;
+
+    @Option(name = "--ipSrc", description = "Source IP Address",
+            required = false, multiValued = false)
+    private String srcIpString = null;
+
+    @Option(name = "--ipDst", description = "Destination IP Address",
+            required = false, multiValued = false)
+    private String dstIpString = null;
+
+    @Option(name = "--tcpSrc", description = "Source TCP Port",
+            required = false, multiValued = false)
+    private String srcTcpString = null;
+
+    @Option(name = "--tcpDst", description = "Destination TCP Port",
+            required = false, multiValued = false)
+    private String dstTcpString = null;
+
     @Option(name = "-b", aliases = "--bandwidth", description = "Bandwidth",
             required = false, multiValued = false)
     private String bandwidthString = "";
@@ -79,6 +100,26 @@
             selectorBuilder.matchEthDst(MacAddress.valueOf(dstMacString));
         }
 
+        if (!isNullOrEmpty(ipProtoString)) {
+            selectorBuilder.matchIPProtocol((byte) Short.parseShort(ipProtoString));
+        }
+
+        if (!isNullOrEmpty(srcIpString)) {
+            selectorBuilder.matchIPSrc(IpPrefix.valueOf(srcIpString));
+        }
+
+        if (!isNullOrEmpty(dstIpString)) {
+            selectorBuilder.matchIPDst(IpPrefix.valueOf(dstIpString));
+        }
+
+        if (!isNullOrEmpty(srcTcpString)) {
+            selectorBuilder.matchTcpSrc((short) Integer.parseInt(srcTcpString));
+        }
+
+        if (!isNullOrEmpty(dstTcpString)) {
+            selectorBuilder.matchTcpSrc((short) Integer.parseInt(dstTcpString));
+        }
+
         return selectorBuilder.build();
     }
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/AnnotationKeys.java b/core/api/src/main/java/org/onlab/onos/net/AnnotationKeys.java
new file mode 100644
index 0000000..5777022
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/AnnotationKeys.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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;
+
+/**
+ * Collection of keys for annotation.
+ * Definitions of annotation keys needs to be here to avoid scattering.
+ */
+public final class AnnotationKeys {
+
+    // Prohibit instantiation
+    private AnnotationKeys() {}
+
+    /**
+     * Annotation key for latency.
+     */
+    public static final String LATENCY = "latency";
+
+    /**
+     * Returns the value annotated object for the specified annotation key.
+     * The annotated value is expected to be String that can be parsed as double.
+     * If parsing fails, the returned value will be 1.0.
+     *
+     * @param annotated annotated object whose annotated value is obtained
+     * @param key key of annotation
+     * @return double value of annotated object for the specified key
+     */
+    public static double getAnnotatedValue(Annotated annotated, String key) {
+        double value;
+        try {
+            value = Double.parseDouble(annotated.annotations().value(key));
+        } catch (NumberFormatException e) {
+            value = 1.0;
+        }
+        return value;
+    }
+}
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 c2d901b..c6cb361 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
@@ -80,6 +80,7 @@
         this.state = FlowEntryState.FAILED;
         this.errType = errType;
         this.errCode = errCode;
+        this.lastSeen = System.currentTimeMillis();
     }
 
     @Override
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 6e59cf5..f31f3c3 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
@@ -58,7 +58,7 @@
     }
 
     public DefaultFlowRule(DeviceId deviceId, TrafficSelector selector,
-            TrafficTreatment treatement, int priority, ApplicationId appId,
+            TrafficTreatment treatment, int priority, ApplicationId appId,
             int timeout, boolean permanent) {
 
         if (priority < FlowRule.MIN_PRIORITY) {
@@ -68,7 +68,7 @@
         this.deviceId = deviceId;
         this.priority = priority;
         this.selector = selector;
-        this.treatment = treatement;
+        this.treatment = treatment;
         this.appId = appId.id();
         this.timeout = timeout;
         this.permanent = permanent;
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 413473f..673773a 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
@@ -63,7 +63,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(criteria);
+        return criteria.hashCode();
     }
 
     @Override
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 de2c7fd..64a56ca 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
@@ -18,7 +18,7 @@
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.net.provider.Provider;
 
-import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.Future;
 
 /**
  * Abstraction of a flow rule provider.
@@ -58,6 +58,6 @@
      * @param batch a batch of flow rules
      * @return a future indicating the status of this execution
      */
-    ListenableFuture<CompletedBatchOperation> executeBatch(BatchOperation<FlowRuleBatchEntry> batch);
+    Future<CompletedBatchOperation> executeBatch(BatchOperation<FlowRuleBatchEntry> batch);
 
 }
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 bac1bab..61fe54d 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
@@ -196,7 +196,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(port, type());
+            return Objects.hash(type(), port);
         }
 
         @Override
@@ -242,7 +242,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(mac, type);
+            return Objects.hash(type, mac);
         }
 
         @Override
@@ -288,7 +288,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(ethType, type());
+            return Objects.hash(type(), ethType);
         }
 
         @Override
@@ -336,7 +336,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(ip, type);
+            return Objects.hash(type, ip);
         }
 
         @Override
@@ -382,7 +382,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(proto, type());
+            return Objects.hash(type(), proto);
         }
 
         @Override
@@ -427,7 +427,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(vlanPcp);
+            return Objects.hash(type(), vlanPcp);
         }
 
         @Override
@@ -474,7 +474,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(vlanId, type());
+            return Objects.hash(type(), vlanId);
         }
 
         @Override
@@ -522,7 +522,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(tcpPort, type);
+            return Objects.hash(type, tcpPort);
         }
 
         @Override
@@ -568,7 +568,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(lambda, type);
+            return Objects.hash(type, lambda);
         }
 
         @Override
@@ -612,7 +612,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(signalType, type);
+            return Objects.hash(type, signalType);
         }
 
         @Override
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 0e77f4a..7dc0f8d 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
@@ -190,7 +190,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(port, type());
+            return Objects.hash(type(), port);
         }
 
         @Override
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L0ModificationInstruction.java b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L0ModificationInstruction.java
index 0d5cd81..25fe79f 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L0ModificationInstruction.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/instructions/L0ModificationInstruction.java
@@ -70,7 +70,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(lambda, type(), subtype);
+            return Objects.hash(type(), subtype, lambda);
         }
 
         @Override
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 abe19e3..20eaf6e 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
@@ -93,7 +93,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(mac, type(), subtype);
+            return Objects.hash(type(), subtype, mac);
         }
 
         @Override
@@ -142,7 +142,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(vlanId, type(), subtype());
+            return Objects.hash(type(), subtype(), vlanId);
         }
 
         @Override
@@ -191,7 +191,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(vlanPcp, type(), subtype());
+            return Objects.hash(type(), subtype(), vlanPcp);
         }
 
         @Override
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 89a8cda..e8b72e7 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
@@ -85,7 +85,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(ip, type(), subtype());
+            return Objects.hash(type(), subtype(), ip);
         }
 
         @Override
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 2269aa0..2a4aaeb 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
@@ -23,6 +23,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -61,7 +62,7 @@
                                  Collection<NetworkResource> resources,
                                  TrafficSelector selector,
                                  TrafficTreatment treatment) {
-        this(id, appId, resources, selector, treatment, null);
+        this(id, appId, resources, selector, treatment, Collections.emptyList());
     }
 
     /**
@@ -87,7 +88,7 @@
         super(id, appId, resources);
         this.selector = checkNotNull(selector);
         this.treatment = checkNotNull(treatment);
-        this.constraints = constraints;
+        this.constraints = checkNotNull(constraints);
     }
 
     /**
@@ -97,7 +98,7 @@
         super();
         this.selector = null;
         this.treatment = null;
-        this.constraints = null;
+        this.constraints = Collections.emptyList();
     }
 
     /**
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 893270a..24a3bca 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
@@ -21,6 +21,7 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 
+import java.util.Collections;
 import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -46,7 +47,7 @@
     public HostToHostIntent(ApplicationId appId, HostId one, HostId two,
                             TrafficSelector selector,
                             TrafficTreatment treatment) {
-        this(appId, one, two, selector, treatment, null);
+        this(appId, one, two, selector, treatment, Collections.emptyList());
     }
 
     /**
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 b4fdcfb..ce1f6b1 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
@@ -22,6 +22,7 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -51,7 +52,7 @@
                                 TrafficTreatment treatment,
                                 Set<Link> links,
                                 ConnectPoint egressPoint) {
-        this(appId, selector , treatment, links, egressPoint, null);
+        this(appId, selector , treatment, links, egressPoint, Collections.emptyList());
     }
 
     /**
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 90907fb..8c8cbb0 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
@@ -22,6 +22,7 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -55,14 +56,7 @@
                                          TrafficTreatment treatment,
                                          Set<ConnectPoint> ingressPoints,
                                          ConnectPoint egressPoint) {
-        super(id(MultiPointToSinglePointIntent.class, selector, treatment,
-                 ingressPoints, egressPoint), appId, null, selector, treatment);
-
-        checkNotNull(ingressPoints);
-        checkArgument(!ingressPoints.isEmpty(), "Ingress point set cannot be empty");
-
-        this.ingressPoints = Sets.newHashSet(ingressPoints);
-        this.egressPoint = checkNotNull(egressPoint);
+        this(appId, selector, treatment, ingressPoints, egressPoint, Collections.emptyList());
     }
 
     /**
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 9f8816d..7a0e365 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
@@ -15,6 +15,7 @@
  */
 package org.onlab.onos.net.intent;
 
+import java.util.Collections;
 import java.util.List;
 
 import com.google.common.base.MoreObjects;
@@ -42,9 +43,7 @@
      */
     public PathIntent(ApplicationId appId, TrafficSelector selector,
                       TrafficTreatment treatment, Path path) {
-        super(id(PathIntent.class, selector, treatment, path), appId,
-              resources(path.links()), selector, treatment);
-        this.path = path;
+        this(appId, selector, treatment, path, Collections.emptyList());
     }
 
     /**
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/AnnotationConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/AnnotationConstraint.java
index ac76303..1767a41 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/AnnotationConstraint.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/AnnotationConstraint.java
@@ -21,6 +21,8 @@
 
 import java.util.Objects;
 
+import static org.onlab.onos.net.AnnotationKeys.getAnnotatedValue;
+
 /**
  * Constraint that evaluates an arbitrary link annotated value is under the specified threshold.
  */
@@ -41,6 +43,12 @@
         this.threshold = threshold;
     }
 
+    // Constructor for serialization
+    private AnnotationConstraint() {
+        this.key = "";
+        this.threshold = 0;
+    }
+
     /**
      * Returns the key of link annotation this constraint designates.
      * @return key of link annotation
@@ -65,25 +73,6 @@
         return value <= threshold;
     }
 
-    /**
-     * Returns the annotated value of the specified link. The annotated value
-     * is expected to be String that can be parsed as double. If parsing fails,
-     * the returned value will be 1.0.
-     *
-     * @param link link whose annotated value is obtained
-     * @param key key of link annotation
-     * @return double value of link annotation for the specified key
-     */
-    private double getAnnotatedValue(Link link, String key) {
-        double value;
-        try {
-            value = Double.parseDouble(link.annotations().value(key));
-        } catch (NumberFormatException e) {
-            value = 1.0;
-        }
-        return value;
-    }
-
     @Override
     public double cost(Link link, LinkResourceService resourceService) {
         if (isValid(link, resourceService)) {
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LatencyConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LatencyConstraint.java
index fcdf330..e4b4432 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LatencyConstraint.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LatencyConstraint.java
@@ -25,14 +25,14 @@
 import java.time.temporal.ChronoUnit;
 import java.util.Objects;
 
+import static org.onlab.onos.net.AnnotationKeys.LATENCY;
+import static org.onlab.onos.net.AnnotationKeys.getAnnotatedValue;
+
 /**
  * Constraint that evaluates the latency through a path.
  */
 public class LatencyConstraint implements Constraint {
 
-    // TODO: formalize the key for latency all over the codes.
-    private static final String LATENCY_KEY = "latency";
-
     private final Duration latency;
 
     /**
@@ -43,22 +43,18 @@
         this.latency = latency;
     }
 
+    // Constructor for serialization
+    private LatencyConstraint() {
+        this.latency = Duration.ZERO;
+    }
+
     public Duration latency() {
         return latency;
     }
 
     @Override
     public double cost(Link link, LinkResourceService resourceService) {
-        String value = link.annotations().value(LATENCY_KEY);
-
-        double latencyInMicroSec;
-        try {
-            latencyInMicroSec = Double.parseDouble(value);
-        } catch (NumberFormatException e) {
-            latencyInMicroSec = 1.0;
-        }
-
-        return latencyInMicroSec;
+        return getAnnotatedValue(link, LATENCY);
     }
 
     @Override
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/ObstacleConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/ObstacleConstraint.java
index 6d73fc2..8472f3c 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/ObstacleConstraint.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/ObstacleConstraint.java
@@ -21,6 +21,7 @@
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.resource.LinkResourceService;
 
+import java.util.Collections;
 import java.util.Objects;
 import java.util.Set;
 
@@ -39,6 +40,11 @@
         this.obstacles = ImmutableSet.copyOf(obstacles);
     }
 
+    // Constructor for serialization
+    private ObstacleConstraint() {
+        this.obstacles = Collections.emptySet();
+    }
+
     @Override
     public boolean isValid(Link link, LinkResourceService resourceService) {
         DeviceId src = link.src().deviceId();
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/WaypointConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/WaypointConstraint.java
index 2a1e3e3..9e3cc20 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/WaypointConstraint.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/WaypointConstraint.java
@@ -17,12 +17,13 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
 import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.resource.LinkResourceService;
 
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
@@ -35,20 +36,25 @@
  */
 public class WaypointConstraint implements Constraint {
 
-    private final List<ElementId> waypoints;
+    private final List<DeviceId> waypoints;
 
     /**
      * Creates a new waypoint constraint.
      *
      * @param waypoints waypoints
      */
-    public WaypointConstraint(ElementId... waypoints) {
+    public WaypointConstraint(DeviceId... waypoints) {
         checkNotNull(waypoints, "waypoints cannot be null");
         checkArgument(waypoints.length > 0, "length of waypoints should be more than 0");
         this.waypoints = ImmutableList.copyOf(waypoints);
     }
 
-    public List<ElementId> waypoints() {
+    // Constructor for serialization
+    private WaypointConstraint() {
+        this.waypoints = Collections.emptyList();
+    }
+
+    public List<DeviceId> waypoints() {
         return waypoints;
     }
 
@@ -60,8 +66,8 @@
 
     @Override
     public boolean validate(Path path, LinkResourceService resourceService) {
-        LinkedList<ElementId> waypoints = new LinkedList<>(this.waypoints);
-        ElementId current = waypoints.poll();
+        LinkedList<DeviceId> waypoints = new LinkedList<>(this.waypoints);
+        DeviceId current = waypoints.poll();
         // This is safe because Path class ensures the number of links are more than 0
         Link firstLink = path.links().get(0);
         if (firstLink.src().elementId().equals(current)) {
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/constraint/LatencyConstraintTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/constraint/LatencyConstraintTest.java
index b9fa3ee..d1a280c 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/constraint/LatencyConstraintTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/constraint/LatencyConstraintTest.java
@@ -37,6 +37,7 @@
 import static org.hamcrest.Matchers.closeTo;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
+import static org.onlab.onos.net.AnnotationKeys.LATENCY;
 import static org.onlab.onos.net.DefaultLinkTest.cp;
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.Link.Type.DIRECT;
@@ -51,7 +52,6 @@
     private static final PortNumber PN3 = PortNumber.portNumber(3);
     private static final PortNumber PN4 = PortNumber.portNumber(4);
     private static final ProviderId PROVIDER_ID = new ProviderId("of", "foo");
-    private static final String LATENCY_KEY = "latency";
     private static final String LATENCY1 = "3.0";
     private static final String LATENCY2 = "4.0";
 
@@ -66,8 +66,8 @@
     public void setUp() {
         linkResourceService = createMock(LinkResourceService.class);
 
-        Annotations annotations1 = DefaultAnnotations.builder().set(LATENCY_KEY, LATENCY1).build();
-        Annotations annotations2 = DefaultAnnotations.builder().set(LATENCY_KEY, LATENCY2).build();
+        Annotations annotations1 = DefaultAnnotations.builder().set(LATENCY, LATENCY1).build();
+        Annotations annotations2 = DefaultAnnotations.builder().set(LATENCY, LATENCY2).build();
 
         link1 = new DefaultLink(PROVIDER_ID, cp(DID1, PN1), cp(DID2, PN2), DIRECT, annotations1);
         link2 = new DefaultLink(PROVIDER_ID, cp(DID2, PN3), cp(DID3, PN4), DIRECT, annotations2);
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 68665e4..b15872b 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
@@ -18,9 +18,13 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_MASTERSHIP_CHANGED;
 import static org.onlab.onos.net.MastershipRole.*;
+import static org.onlab.util.Tools.namedThreads;
 import static org.slf4j.LoggerFactory.getLogger;
 
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -83,6 +87,8 @@
 
     private final MastershipListener mastershipListener = new InternalMastershipListener();
 
+    private ScheduledExecutorService backgroundService;
+
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DeviceStore store;
 
@@ -102,15 +108,31 @@
 
     @Activate
     public void activate() {
+        backgroundService = Executors.newSingleThreadScheduledExecutor(namedThreads("device-manager-background"));
+
         store.setDelegate(delegate);
         eventDispatcher.addSink(DeviceEvent.class, listenerRegistry);
         mastershipService.addListener(mastershipListener);
         termService = mastershipService.requestTermService();
+
+        backgroundService.scheduleWithFixedDelay(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    mastershipCheck();
+                } catch (Exception e) {
+                    log.error("Exception thrown during integrity check", e);
+                }
+            }
+        }, 1, 1, TimeUnit.MINUTES);
         log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
+        backgroundService.shutdown();
+
         store.unsetDelegate(delegate);
         mastershipService.removeListener(mastershipListener);
         eventDispatcher.removeSink(DeviceEvent.class);
@@ -172,10 +194,6 @@
     @Override
     public void removeDevice(DeviceId deviceId) {
         checkNotNull(deviceId, DEVICE_ID_NULL);
-        // XXX is this intended to apply to the full global topology?
-        // if so, we probably don't want the fact that we aren't
-        // MASTER to get in the way, as it would do now.
-        // FIXME: forward or broadcast and let the Master handler the event.
         DeviceEvent event = store.removeDevice(deviceId);
         if (event != null) {
             log.info("Device {} administratively removed", deviceId);
@@ -199,6 +217,31 @@
         return new InternalDeviceProviderService(provider);
     }
 
+    /**
+     * Checks if all the reachable devices have a valid mastership role.
+     */
+    private void mastershipCheck() {
+        log.debug("Checking mastership");
+        for (Device device : getDevices()) {
+            final DeviceId deviceId = device.id();
+            log.debug("Checking device {}", deviceId);
+
+            if (!isReachable(deviceId)) {
+                continue;
+            }
+
+            if (mastershipService.getLocalRole(deviceId) != NONE) {
+                continue;
+            }
+
+            log.info("{} is reachable but did not have a valid role, reasserting", deviceId);
+
+            // isReachable but was not MASTER or STANDBY, get a role and apply
+            // Note: NONE triggers request to MastershipService
+            reassertRole(deviceId, NONE);
+        }
+    }
+
     // Personalized device provider service issued to the supplied provider.
     private class InternalDeviceProviderService
             extends AbstractProviderService<DeviceProvider>
@@ -418,48 +461,112 @@
         }
     }
 
-    // Intercepts mastership events
-    private class InternalMastershipListener implements MastershipListener {
-
-        // Applies the specified role to the device; ignores NONE
-        /**
-         * Apply role in reaction to mastership event.
-         *
-         * @param deviceId  device identifier
-         * @param newRole   new role to apply to the device
-         * @return true if the request was sent to provider
-         */
-        private boolean applyRole(DeviceId deviceId, MastershipRole newRole) {
-            if (newRole.equals(MastershipRole.NONE)) {
-                //no-op
-                return true;
-            }
-
-            Device device = store.getDevice(deviceId);
-            // FIXME: Device might not be there yet. (eventual consistent)
-            // FIXME relinquish role
-            if (device == null) {
-                log.warn("{} was not there. Cannot apply role {}", deviceId, newRole);
-                return false;
-            }
-
-            DeviceProvider provider = getProvider(device.providerId());
-            if (provider == null) {
-                log.warn("Provider for {} was not found. Cannot apply role {}", deviceId, newRole);
-                return false;
-            }
-            provider.roleChanged(deviceId, newRole);
-
-            if (newRole.equals(MastershipRole.MASTER)) {
-                // only trigger event when request was sent to provider
-                // TODO: consider removing this from Device event type?
-                post(new DeviceEvent(DEVICE_MASTERSHIP_CHANGED, device));
-
-                provider.triggerProbe(device);
-            }
+    // Applies the specified role to the device; ignores NONE
+    /**
+     * Apply role to device and send probe if MASTER.
+     *
+     * @param deviceId  device identifier
+     * @param newRole   new role to apply to the device
+     * @return true if the request was sent to provider
+     */
+    private boolean applyRoleAndProbe(DeviceId deviceId, MastershipRole newRole) {
+        if (newRole.equals(MastershipRole.NONE)) {
+            //no-op
             return true;
         }
 
+        Device device = store.getDevice(deviceId);
+        // FIXME: Device might not be there yet. (eventual consistent)
+        // FIXME relinquish role
+        if (device == null) {
+            log.warn("{} was not there. Cannot apply role {}", deviceId, newRole);
+            return false;
+        }
+
+        DeviceProvider provider = getProvider(device.providerId());
+        if (provider == null) {
+            log.warn("Provider for {} was not found. Cannot apply role {}", deviceId, newRole);
+            return false;
+        }
+        provider.roleChanged(deviceId, newRole);
+
+        if (newRole.equals(MastershipRole.MASTER)) {
+            // only trigger event when request was sent to provider
+            // TODO: consider removing this from Device event type?
+            post(new DeviceEvent(DEVICE_MASTERSHIP_CHANGED, device));
+
+            provider.triggerProbe(device);
+        }
+        return true;
+    }
+
+    /**
+     * Reaasert role for specified device connected to this node.
+     *
+     * @param did         device identifier
+     * @param nextRole    role to apply. If NONE is specified,
+     *        it will ask mastership service for a role and apply it.
+     */
+    private void reassertRole(final DeviceId did,
+                              final MastershipRole nextRole) {
+
+        final NodeId myNodeId = clusterService.getLocalNode().id();
+        MastershipRole myNextRole = nextRole;
+        if (myNextRole == NONE) {
+            mastershipService.requestRoleFor(did);
+            MastershipTerm term = termService.getMastershipTerm(did);
+            if (myNodeId.equals(term.master())) {
+                myNextRole = MASTER;
+            } else {
+                myNextRole = STANDBY;
+            }
+        }
+
+        switch (myNextRole) {
+        case MASTER:
+            final Device device = getDevice(did);
+            if ((device != null) && !isAvailable(did)) {
+                //flag the device as online. Is there a better way to do this?
+                DefaultDeviceDescription deviceDescription
+                    = new DefaultDeviceDescription(did.uri(),
+                                                   device.type(),
+                                                   device.manufacturer(),
+                                                   device.hwVersion(),
+                                                   device.swVersion(),
+                                                   device.serialNumber(),
+                                                   device.chassisId());
+                DeviceEvent devEvent =
+                        store.createOrUpdateDevice(device.providerId(), did,
+                                                   deviceDescription);
+                post(devEvent);
+            }
+            // TODO: should apply role only if there is mismatch
+            log.info("Applying role {} to {}", myNextRole, did);
+            if (!applyRoleAndProbe(did, MASTER)) {
+                // immediately failed to apply role
+                mastershipService.relinquishMastership(did);
+                // FIXME disconnect?
+            }
+            break;
+        case STANDBY:
+            log.info("Applying role {} to {}", myNextRole, did);
+            if (!applyRoleAndProbe(did, STANDBY)) {
+                // immediately failed to apply role
+                mastershipService.relinquishMastership(did);
+                // FIXME disconnect?
+            }
+            break;
+        case NONE:
+        default:
+            // should never reach here
+            log.error("You didn't see anything. I did not exist.");
+            break;
+        }
+    }
+
+    // Intercepts mastership events
+    private class InternalMastershipListener implements MastershipListener {
+
         @Override
         public void event(MastershipEvent event) {
 
@@ -499,55 +606,12 @@
                             + "Relinquishing role.  ",
                              myNextRole, did);
                     mastershipService.relinquishMastership(did);
-                    // FIXME disconnect?
                 }
                 return;
             }
 
             // device is connected to this node:
-
-            if (myNextRole == NONE) {
-                mastershipService.requestRoleFor(did);
-                MastershipTerm term = termService.getMastershipTerm(did);
-                if (myNodeId.equals(term.master())) {
-                    myNextRole = MASTER;
-                } else {
-                    myNextRole = STANDBY;
-                }
-            }
-
-            switch (myNextRole) {
-            case MASTER:
-                final Device device = getDevice(did);
-                if ((device != null) && !isAvailable(did)) {
-                    //flag the device as online. Is there a better way to do this?
-                    DefaultDeviceDescription deviceDescription
-                        = new DefaultDeviceDescription(did.uri(),
-                                                       device.type(),
-                                                       device.manufacturer(),
-                                                       device.hwVersion(),
-                                                       device.swVersion(),
-                                                       device.serialNumber(),
-                                                       device.chassisId());
-                    DeviceEvent devEvent =
-                            store.createOrUpdateDevice(device.providerId(), did,
-                                                       deviceDescription);
-                    post(devEvent);
-                }
-                // TODO: should apply role only if there is mismatch
-                log.info("Applying role {} to {}", myNextRole, did);
-                applyRole(did, MASTER);
-                break;
-            case STANDBY:
-                log.info("Applying role {} to {}", myNextRole, did);
-                applyRole(did, STANDBY);
-                break;
-            case NONE:
-            default:
-                // should never reach here
-                log.error("You didn't see anything. I did not exist.");
-                break;
-            }
+            reassertRole(did, myNextRole);
         }
     }
 
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 e996dfc..2e53252 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
@@ -15,21 +15,12 @@
  */
 package org.onlab.onos.net.flow.impl;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.slf4j.LoggerFactory.getLogger;
-import static org.onlab.util.Tools.namedThreads;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -64,14 +55,22 @@
 import org.onlab.onos.net.provider.AbstractProviderService;
 import org.slf4j.Logger;
 
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.namedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Provides implementation of the flow NB &amp; SB APIs.
@@ -92,8 +91,7 @@
 
     private final FlowRuleStoreDelegate delegate = new InternalStoreDelegate();
 
-    private final ExecutorService futureListeners =
-            Executors.newCachedThreadPool(namedThreads("provider-future-listeners"));
+    private ExecutorService futureService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected FlowRuleStore store;
@@ -106,6 +104,7 @@
 
     @Activate
     public void activate() {
+        futureService = Executors.newCachedThreadPool(namedThreads("provider-future-listeners"));
         store.setDelegate(delegate);
         eventDispatcher.addSink(FlowRuleEvent.class, listenerRegistry);
         log.info("Started");
@@ -113,7 +112,7 @@
 
     @Deactivate
     public void deactivate() {
-        futureListeners.shutdownNow();
+        futureService.shutdownNow();
 
         store.unsetDelegate(delegate);
         eventDispatcher.removeSink(FlowRuleEvent.class);
@@ -364,6 +363,9 @@
 
     // Store delegate to re-post events emitted from the store.
     private class InternalStoreDelegate implements FlowRuleStoreDelegate {
+
+        private static final int TIMEOUT = 5000; // ms
+
         // TODO: Right now we only dispatch events at individual flowEntry level.
         // It may be more efficient for also dispatch events as a batch.
         @Override
@@ -384,15 +386,28 @@
 
                 FlowRuleProvider flowRuleProvider =
                         getProvider(batchOperation.getOperations().get(0).getTarget().deviceId());
-                final ListenableFuture<CompletedBatchOperation> result =
+                final Future<CompletedBatchOperation> result =
                         flowRuleProvider.executeBatch(batchOperation);
-                result.addListener(new Runnable() {
+                futureService.submit(new Runnable() {
                     @Override
                     public void run() {
-                        store.batchOperationComplete(FlowRuleBatchEvent.completed(request,
-                                                                                  Futures.getUnchecked(result)));
+                        CompletedBatchOperation res;
+                        try {
+                            res = result.get(TIMEOUT, TimeUnit.MILLISECONDS);
+                            store.batchOperationComplete(FlowRuleBatchEvent.completed(request, res));
+                        } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                            log.warn("Something went wrong with the batch operation {}",
+                                     request.batchId(), e);
+
+                            Set<FlowRule> failures = new HashSet<>(batchOperation.size());
+                            for (FlowRuleBatchEntry op : batchOperation.getOperations()) {
+                                failures.add(op.getTarget());
+                            }
+                            res = new CompletedBatchOperation(false, failures);
+                            store.batchOperationComplete(FlowRuleBatchEvent.completed(request, res));
+                        }
                     }
-                }, futureListeners);
+                });
                 break;
 
             case BATCH_OPERATION_COMPLETED:
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
index d66e849..405366d 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
@@ -15,6 +15,8 @@
  */
 package org.onlab.onos.net.intent.impl;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Reference;
@@ -94,11 +96,19 @@
     protected Path getPath(ConnectivityIntent intent,
                            ElementId one, ElementId two) {
         Set<Path> paths = pathService.getPaths(one, two, weight(intent.constraints()));
-        if (paths.isEmpty()) {
-            throw new PathNotFoundException("No packet path from " + one + " to " + two);
+        final List<Constraint> constraints = intent.constraints();
+        ImmutableList<Path> filtered = FluentIterable.from(paths)
+                .filter(new Predicate<Path>() {
+                    @Override
+                    public boolean apply(Path path) {
+                        return checkPath(path, constraints);
+                    }
+                }).toList();
+        if (filtered.isEmpty()) {
+            throw new PathNotFoundException("No packet path form " + one + " to " + two);
         }
         // TODO: let's be more intelligent about this eventually
-        return paths.iterator().next();
+        return filtered.iterator().next();
     }
 
     /**
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java b/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java
new file mode 100644
index 0000000..b8d7799
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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.TimeUnit;
+
+import org.junit.Test;
+import org.onlab.onos.net.intent.IntentTestsMocks;
+
+import com.google.common.testing.EqualsTester;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.onos.net.NetTestTools.did;
+
+/**
+ * Unit tests for the DefaultFlowEntry class.
+ */
+public class DefaultFlowEntryTest {
+    private static final IntentTestsMocks.MockSelector SELECTOR =
+            new IntentTestsMocks.MockSelector();
+    private static final IntentTestsMocks.MockTreatment TREATMENT =
+            new IntentTestsMocks.MockTreatment();
+
+    private static DefaultFlowEntry makeFlowEntry(int uniqueValue) {
+        return new DefaultFlowEntry(did("id" + Integer.toString(uniqueValue)),
+                SELECTOR,
+                TREATMENT,
+                uniqueValue,
+                FlowEntry.FlowEntryState.ADDED,
+                uniqueValue,
+                uniqueValue,
+                uniqueValue,
+                uniqueValue,
+                uniqueValue);
+    }
+
+    final DefaultFlowEntry defaultFlowEntry1 = makeFlowEntry(1);
+    final DefaultFlowEntry sameAsDefaultFlowEntry1 = makeFlowEntry(1);
+    final DefaultFlowEntry defaultFlowEntry2 = makeFlowEntry(2);
+
+    /**
+     * Tests the equals, hashCode and toString methods using Guava EqualsTester.
+     */
+    @Test
+    public void testEquals() {
+        new EqualsTester()
+                .addEqualityGroup(defaultFlowEntry1, sameAsDefaultFlowEntry1)
+                .addEqualityGroup(defaultFlowEntry2)
+                .testEquals();
+    }
+
+    /**
+     * Tests the construction of a default flow entry from a device id.
+     */
+    @Test
+    public void testDeviceBasedObject() {
+        assertThat(defaultFlowEntry1.deviceId(), is(did("id1")));
+        assertThat(defaultFlowEntry1.selector(), is(SELECTOR));
+        assertThat(defaultFlowEntry1.treatment(), is(TREATMENT));
+        assertThat(defaultFlowEntry1.timeout(), is(1));
+        assertThat(defaultFlowEntry1.life(), is(1L));
+        assertThat(defaultFlowEntry1.packets(), is(1L));
+        assertThat(defaultFlowEntry1.bytes(), is(1L));
+        assertThat(defaultFlowEntry1.state(), is(FlowEntry.FlowEntryState.ADDED));
+        assertThat(defaultFlowEntry1.lastSeen(),
+                   greaterThan(System.currentTimeMillis() -
+                           TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS)));
+    }
+
+    /**
+     * Tests the setters on a default flow entry object.
+     */
+    @Test
+    public void testSetters() {
+        final DefaultFlowEntry entry = makeFlowEntry(1);
+
+        entry.setLastSeen();
+        entry.setState(FlowEntry.FlowEntryState.PENDING_REMOVE);
+        entry.setPackets(11);
+        entry.setBytes(22);
+        entry.setLife(33);
+
+        assertThat(entry.deviceId(), is(did("id1")));
+        assertThat(entry.selector(), is(SELECTOR));
+        assertThat(entry.treatment(), is(TREATMENT));
+        assertThat(entry.timeout(), is(1));
+        assertThat(entry.life(), is(33L));
+        assertThat(entry.packets(), is(11L));
+        assertThat(entry.bytes(), is(22L));
+        assertThat(entry.state(), is(FlowEntry.FlowEntryState.PENDING_REMOVE));
+        assertThat(entry.lastSeen(),
+                greaterThan(System.currentTimeMillis() -
+                        TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS)));
+    }
+
+    /**
+     * Tests a default flow rule built for an error.
+     */
+    @Test
+    public void testErrorObject() {
+        final DefaultFlowEntry errorEntry =
+                new DefaultFlowEntry(new IntentTestsMocks.MockFlowRule(1),
+                                     111,
+                                     222);
+        assertThat(errorEntry.errType(), is(111));
+        assertThat(errorEntry.errCode(), is(222));
+        assertThat(errorEntry.state(), is(FlowEntry.FlowEntryState.FAILED));
+        assertThat(errorEntry.lastSeen(),
+                greaterThan(System.currentTimeMillis() -
+                        TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS)));
+    }
+
+    /**
+     * Tests a default flow entry constructed from a flow rule.
+     */
+    @Test
+    public void testFlowBasedObject() {
+        final DefaultFlowEntry entry =
+                new DefaultFlowEntry(new IntentTestsMocks.MockFlowRule(1));
+        assertThat(entry.priority(), is(1));
+        assertThat(entry.appId(), is((short) 0));
+        assertThat(entry.lastSeen(),
+                greaterThan(System.currentTimeMillis() -
+                        TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS)));
+    }
+
+    /**
+     * Tests a default flow entry constructed from a flow rule plus extra
+     * parameters.
+     */
+    @Test
+    public void testFlowBasedObjectWithParameters() {
+        final DefaultFlowEntry entry =
+                new DefaultFlowEntry(new IntentTestsMocks.MockFlowRule(33),
+                        FlowEntry.FlowEntryState.REMOVED,
+                        101, 102, 103);
+        assertThat(entry.state(), is(FlowEntry.FlowEntryState.REMOVED));
+        assertThat(entry.life(), is(101L));
+        assertThat(entry.packets(), is(102L));
+        assertThat(entry.bytes(), is(103L));
+        assertThat(entry.lastSeen(),
+                greaterThan(System.currentTimeMillis() -
+                        TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS)));
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java b/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java
new file mode 100644
index 0000000..f2c418f
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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.junit.Test;
+import org.onlab.onos.net.intent.IntentTestsMocks;
+
+import com.google.common.testing.EqualsTester;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutableBaseClass;
+import static org.onlab.onos.net.NetTestTools.APP_ID;
+import static org.onlab.onos.net.NetTestTools.did;
+
+/**
+ * Unit tests for the default flow rule class.
+ */
+public class DefaultFlowRuleTest {
+    private static final IntentTestsMocks.MockSelector SELECTOR =
+            new IntentTestsMocks.MockSelector();
+    private static final IntentTestsMocks.MockTreatment TREATMENT =
+            new IntentTestsMocks.MockTreatment();
+
+    final FlowRule flowRule1 = new IntentTestsMocks.MockFlowRule(1);
+    final FlowRule sameAsFlowRule1 = new IntentTestsMocks.MockFlowRule(1);
+    final FlowRule flowRule2 = new IntentTestsMocks.MockFlowRule(2);
+    final DefaultFlowRule defaultFlowRule1 = new DefaultFlowRule(flowRule1);
+    final DefaultFlowRule sameAsDefaultFlowRule1 = new DefaultFlowRule(sameAsFlowRule1);
+    final DefaultFlowRule defaultFlowRule2 = new DefaultFlowRule(flowRule2);
+
+    /**
+     * Checks that the DefaultFlowRule class is immutable but can be inherited
+     * from.
+     */
+    @Test
+    public void testImmutability() {
+        assertThatClassIsImmutableBaseClass(DefaultFlowRule.class);
+    }
+
+    /**
+     * Tests the equals, hashCode and toString methods using Guava EqualsTester.
+     */
+    @Test
+    public void testEquals() {
+        new EqualsTester()
+                .addEqualityGroup(defaultFlowRule1, sameAsDefaultFlowRule1)
+                .addEqualityGroup(defaultFlowRule2)
+                .testEquals();
+    }
+
+    /**
+     * Tests creation of a DefaultFlowRule using a FlowRule constructor.
+     */
+    @Test
+    public void testCreationFromFlowRule() {
+        assertThat(defaultFlowRule1.deviceId(), is(flowRule1.deviceId()));
+        assertThat(defaultFlowRule1.appId(), is(flowRule1.appId()));
+        assertThat(defaultFlowRule1.id(), is(flowRule1.id()));
+        assertThat(defaultFlowRule1.isPermanent(), is(flowRule1.isPermanent()));
+        assertThat(defaultFlowRule1.priority(), is(flowRule1.priority()));
+        assertThat(defaultFlowRule1.selector(), is(flowRule1.selector()));
+        assertThat(defaultFlowRule1.treatment(), is(flowRule1.treatment()));
+        assertThat(defaultFlowRule1.timeout(), is(flowRule1.timeout()));
+    }
+
+    /**
+     * Tests creation of a DefaultFlowRule using a FlowId constructor.
+     */
+    @Test
+    public void testCreationWithFlowId() {
+        final DefaultFlowRule rule =
+                new DefaultFlowRule(did("1"), SELECTOR,
+                        TREATMENT, 22, 33,
+                44, false);
+        assertThat(rule.deviceId(), is(did("1")));
+        assertThat(rule.id().value(), is(33L));
+        assertThat(rule.isPermanent(), is(false));
+        assertThat(rule.priority(), is(22));
+        assertThat(rule.selector(), is(SELECTOR));
+        assertThat(rule.treatment(), is(TREATMENT));
+        assertThat(rule.timeout(), is(44));
+    }
+
+    /**
+     * Tests the creation of a DefaultFlowRule using an AppId constructor.
+     */
+    @Test
+    public void testCreationWithAppId() {
+        final DefaultFlowRule rule =
+                new DefaultFlowRule(did("1"), SELECTOR,
+                        TREATMENT, 22, APP_ID,
+                        44, false);
+        assertThat(rule.deviceId(), is(did("1")));
+        assertThat(rule.isPermanent(), is(false));
+        assertThat(rule.priority(), is(22));
+        assertThat(rule.selector(), is(SELECTOR));
+        assertThat(rule.treatment(), is(TREATMENT));
+        assertThat(rule.timeout(), is(44));
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
index e0fe09e..65df6b2 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
@@ -16,6 +16,7 @@
 package org.onlab.onos.net.intent;
 
 import static org.onlab.onos.net.NetTestTools.createPath;
+import static org.onlab.onos.net.NetTestTools.did;
 import static org.onlab.onos.net.NetTestTools.link;
 
 import java.util.ArrayList;
@@ -31,6 +32,8 @@
 import org.onlab.onos.net.ElementId;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
+import org.onlab.onos.net.flow.FlowId;
+import org.onlab.onos.net.flow.FlowRule;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.flow.criteria.Criterion;
@@ -271,4 +274,60 @@
         }
     }
 
+    private static final IntentTestsMocks.MockSelector SELECTOR =
+            new IntentTestsMocks.MockSelector();
+    private static final IntentTestsMocks.MockTreatment TREATMENT =
+            new IntentTestsMocks.MockTreatment();
+
+    public static class MockFlowRule implements FlowRule {
+
+        int priority;
+        public MockFlowRule(int priority) {
+            this.priority = priority;
+        }
+
+        @Override
+        public FlowId id() {
+            return FlowId.valueOf(1);
+        }
+
+        @Override
+        public short appId() {
+            return 0;
+        }
+
+        @Override
+        public int priority() {
+            return priority;
+        }
+
+        @Override
+        public DeviceId deviceId() {
+            return did("1");
+        }
+
+        @Override
+        public TrafficSelector selector() {
+            return SELECTOR;
+        }
+
+        @Override
+        public TrafficTreatment treatment() {
+            return TREATMENT;
+        }
+
+        @Override
+        public int timeout() {
+            return 0;
+        }
+
+        @Override
+        public boolean isPermanent() {
+            return false;
+        }
+
+
+    }
+
+
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
index f65da3f..c94b6c0 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
@@ -85,6 +85,8 @@
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -132,8 +134,7 @@
     private Cache<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures =
             CacheBuilder.newBuilder()
                 .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES)
-                // TODO Explicitly fail the future if expired?
-                //.removalListener(listener)
+                .removalListener(new TimeoutFuture())
                 .build();
 
     // Cache of SMaps used for backup data.  each SMap contain device flow table
@@ -541,6 +542,17 @@
         log.debug("removedFromPrimary {}", removed);
     }
 
+    private static final class TimeoutFuture
+        implements RemovalListener<Integer, SettableFuture<CompletedBatchOperation>> {
+        @Override
+        public void onRemoval(RemovalNotification<Integer, SettableFuture<CompletedBatchOperation>> notification) {
+            // wrapping in ExecutionException to support Future.get
+            notification.getValue()
+                .setException(new ExecutionException("Timed out",
+                                                     new TimeoutException()));
+        }
+    }
+
     private final class OnStoreBatch implements ClusterMessageHandler {
         private final NodeId local;
 
@@ -580,7 +592,18 @@
 
                 @Override
                 public void run() {
-                     CompletedBatchOperation result = Futures.getUnchecked(f);
+                    CompletedBatchOperation result;
+                    try {
+                        result = f.get();
+                    } catch (InterruptedException | ExecutionException e) {
+                        log.error("Batch operation failed", e);
+                        // create everything failed response
+                        Set<FlowRule> failures = new HashSet<>(operation.size());
+                        for (FlowRuleBatchEntry op : operation.getOperations()) {
+                            failures.add(op.getTarget());
+                        }
+                        result = new CompletedBatchOperation(false, failures);
+                    }
                     try {
                         message.respond(SERIALIZER.encode(result));
                     } catch (IOException e) {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
index 0d06e08..afeaef9 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
@@ -72,7 +72,7 @@
     public static final String LOG_FILE_PREFIX = "/tmp/onos-copy-cat-log_";
 
     // Current working dir seems to be /opt/onos/apache-karaf-3.0.2
-    // TODO: Get the path to /opt/onos/config
+    // TODO: Set the path to /opt/onos/config
     private static final String CONFIG_DIR = "../config";
 
     private static final String DEFAULT_MEMBER_FILE = "tablets.json";
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 4cba9f0..5d689a3 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
@@ -75,10 +75,14 @@
 import org.onlab.onos.net.intent.OpticalPathIntent;
 import org.onlab.onos.net.intent.PathIntent;
 import org.onlab.onos.net.intent.PointToPointIntent;
+import org.onlab.onos.net.intent.constraint.AnnotationConstraint;
 import org.onlab.onos.net.intent.constraint.BandwidthConstraint;
 import org.onlab.onos.net.intent.constraint.BooleanConstraint;
 import org.onlab.onos.net.intent.constraint.LambdaConstraint;
+import org.onlab.onos.net.intent.constraint.LatencyConstraint;
 import org.onlab.onos.net.intent.constraint.LinkTypeConstraint;
+import org.onlab.onos.net.intent.constraint.ObstacleConstraint;
+import org.onlab.onos.net.intent.constraint.WaypointConstraint;
 import org.onlab.onos.net.link.DefaultLinkDescription;
 import org.onlab.onos.net.packet.DefaultOutboundPacket;
 import org.onlab.onos.net.provider.ProviderId;
@@ -208,9 +212,14 @@
                     LinkResourceRequest.class,
                     Lambda.class,
                     Bandwidth.class,
+                    // Constraints
                     LambdaConstraint.class,
                     BandwidthConstraint.class,
                     LinkTypeConstraint.class,
+                    LatencyConstraint.class,
+                    WaypointConstraint.class,
+                    ObstacleConstraint.class,
+                    AnnotationConstraint.class,
                     BooleanConstraint.class
                     )
             .register(DefaultApplicationId.class, new DefaultApplicationIdSerializer())
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 b7d26fb..622e2de 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
@@ -18,6 +18,8 @@
 import com.google.common.base.Function;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
 import com.google.common.collect.FluentIterable;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.SettableFuture;
@@ -53,8 +55,10 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
@@ -86,8 +90,7 @@
     private Cache<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures =
             CacheBuilder.newBuilder()
                 .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES)
-                // TODO Explicitly fail the future if expired?
-                //.removalListener(listener)
+                .removalListener(new TimeoutFuture())
                 .build();
 
     @Activate
@@ -303,4 +306,15 @@
         }
         notifyDelegate(event);
     }
+
+    private static final class TimeoutFuture
+        implements RemovalListener<Integer, SettableFuture<CompletedBatchOperation>> {
+        @Override
+        public void onRemoval(RemovalNotification<Integer, SettableFuture<CompletedBatchOperation>> notification) {
+            // wrapping in ExecutionException to support Future.get
+            notification.getValue()
+                .setException(new ExecutionException("Timed out",
+                                                     new TimeoutException()));
+        }
+    }
 }
diff --git a/features/features.xml b/features/features.xml
index efd31cf..5ede89c 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -197,6 +197,8 @@
     <feature name="onos-app-sdnip" version="1.0.0"
              description="SDN-IP peering application">
         <feature>onos-api</feature>
+        <feature>onos-app-proxyarp</feature>
+        <feature>onos-app-config</feature>
         <bundle>mvn:org.onlab.onos/onos-app-sdnip/1.0.0-SNAPSHOT</bundle>
     </feature>
 
@@ -225,4 +227,12 @@
         <bundle>mvn:org.onlab.onos/onos-app-metrics-topology/1.0.0-SNAPSHOT</bundle>
     </feature>
 
+    <feature name="onos-app-demo" version="1.0.0"
+             description="ONOS demo applications">
+        <feature>onos-api</feature>
+        <bundle>mvn:org.onlab.onos/onos-app-demo/1.0.0-SNAPSHOT</bundle>
+    </feature>
+
+
+
 </features>
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 018d6f3..214bde3 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
@@ -15,21 +15,11 @@
  */
 package org.onlab.onos.provider.of.flow.impl;
 
-import static org.slf4j.LoggerFactory.getLogger;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ExecutionList;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -80,15 +70,23 @@
 import org.projectfloodlight.openflow.protocol.instruction.OFInstruction;
 import org.projectfloodlight.openflow.protocol.instruction.OFInstructionApplyActions;
 import org.projectfloodlight.openflow.types.OFPort;
-import org.projectfloodlight.openflow.types.U32;
 import org.slf4j.Logger;
 
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.ExecutionList;
-import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Provider which uses an OpenFlow controller to detect network
@@ -124,6 +122,8 @@
 
     private final Map<Dpid, FlowStatsCollector> collectors = Maps.newHashMap();
 
+    private final AtomicLong xidCounter = new AtomicLong(0);
+
     /**
      * Creates an OpenFlow host provider.
      */
@@ -154,6 +154,7 @@
 
         log.info("Stopped");
     }
+
     @Override
     public void applyFlowRule(FlowRule... flowRules) {
         for (int i = 0; i < flowRules.length; i++) {
@@ -167,7 +168,6 @@
     }
 
 
-
     @Override
     public void removeFlowRule(FlowRule... flowRules) {
         for (int i = 0; i < flowRules.length; i++) {
@@ -188,11 +188,15 @@
     }
 
     @Override
-    public ListenableFuture<CompletedBatchOperation> executeBatch(BatchOperation<FlowRuleBatchEntry> batch) {
+    public Future<CompletedBatchOperation> executeBatch(BatchOperation<FlowRuleBatchEntry> batch) {
         final Set<Dpid> sws =
                 Collections.newSetFromMap(new ConcurrentHashMap<Dpid, Boolean>());
         final Map<Long, FlowRuleBatchEntry> fmXids = new HashMap<Long, FlowRuleBatchEntry>();
-        OFFlowMod mod = null;
+        /*
+         * Use identity hash map for reference equality as we could have equal
+         * flow mods for different switches.
+         */
+        Map<OFFlowMod, OpenFlowSwitch> mods = Maps.newIdentityHashMap();
         for (FlowRuleBatchEntry fbe : batch.getOperations()) {
             FlowRule flowRule = fbe.getTarget();
             OpenFlowSwitch sw = controller.getSwitch(Dpid.dpid(flowRule.deviceId().uri()));
@@ -208,6 +212,7 @@
             }
             sws.add(new Dpid(sw.getId()));
             FlowModBuilder builder = FlowModBuilder.builder(flowRule, sw.factory());
+            OFFlowMod mod = null;
             switch (fbe.getOperator()) {
                 case ADD:
                     mod = builder.buildFlowAdd();
@@ -222,25 +227,29 @@
                     log.error("Unsupported batch operation {}", fbe.getOperator());
             }
             if (mod != null) {
-                sw.sendMsg(mod);
-                fmXids.put(mod.getXid(), fbe);
+                mods.put(mod, sw);
+                fmXids.put(xidCounter.getAndIncrement(), fbe);
             } else {
                 log.error("Conversion of flowrule {} failed.", flowRule);
             }
-
         }
         InstallationFuture installation = new InstallationFuture(sws, fmXids);
         for (Long xid : fmXids.keySet()) {
             pendingFMs.put(xid, installation);
         }
-        pendingFutures.put(U32.f(batch.hashCode()), installation);
-        installation.verify(U32.f(batch.hashCode()));
+        pendingFutures.put(installation.xid(), installation);
+        for (Map.Entry<OFFlowMod, OpenFlowSwitch> entry : mods.entrySet()) {
+            OpenFlowSwitch sw = entry.getValue();
+            OFFlowMod mod = entry.getKey();
+            sw.sendMsg(mod);
+        }
+        installation.verify();
         return installation;
     }
 
 
     private class InternalFlowProvider
-    implements OpenFlowSwitchListener, OpenFlowEventListener {
+            implements OpenFlowSwitchListener, OpenFlowEventListener {
 
 
         private final Multimap<DeviceId, FlowEntry> completeEntries =
@@ -274,36 +283,36 @@
         public void handleMessage(Dpid dpid, OFMessage msg) {
             InstallationFuture future = null;
             switch (msg.getType()) {
-            case FLOW_REMOVED:
-                OFFlowRemoved removed = (OFFlowRemoved) msg;
+                case FLOW_REMOVED:
+                    OFFlowRemoved removed = (OFFlowRemoved) msg;
 
-                FlowEntry fr = new FlowEntryBuilder(dpid, removed).build();
-                providerService.flowRemoved(fr);
-                break;
-            case STATS_REPLY:
-                pushFlowMetrics(dpid, (OFStatsReply) msg);
-                break;
-            case BARRIER_REPLY:
-                future = pendingFutures.get(msg.getXid());
-                if (future != null) {
-                    future.satisfyRequirement(dpid);
-                }
-                break;
-            case ERROR:
-                future = pendingFMs.get(msg.getXid());
-                if (future != null) {
-                    future.fail((OFErrorMsg) msg, dpid);
-                }
-                break;
-            default:
-                log.debug("Unhandled message type: {}", msg.getType());
+                    FlowEntry fr = new FlowEntryBuilder(dpid, removed).build();
+                    providerService.flowRemoved(fr);
+                    break;
+                case STATS_REPLY:
+                    pushFlowMetrics(dpid, (OFStatsReply) msg);
+                    break;
+                case BARRIER_REPLY:
+                    future = pendingFutures.get(msg.getXid());
+                    if (future != null) {
+                        future.satisfyRequirement(dpid);
+                    }
+                    break;
+                case ERROR:
+                    future = pendingFMs.get(msg.getXid());
+                    if (future != null) {
+                        future.fail((OFErrorMsg) msg, dpid);
+                    }
+                    break;
+                default:
+                    log.debug("Unhandled message type: {}", msg.getType());
             }
 
         }
 
         @Override
         public void receivedRoleReply(Dpid dpid, RoleState requested,
-                RoleState response) {
+                                      RoleState response) {
             // Do nothing here for now.
         }
 
@@ -352,8 +361,9 @@
 
     }
 
-    private class InstallationFuture implements ListenableFuture<CompletedBatchOperation> {
+    private class InstallationFuture implements Future<CompletedBatchOperation> {
 
+        private final Long xid;
         private final Set<Dpid> sws;
         private final AtomicBoolean ok = new AtomicBoolean(true);
         private final Map<Long, FlowRuleBatchEntry> fms;
@@ -361,18 +371,22 @@
         private final Set<FlowEntry> offendingFlowMods = Sets.newHashSet();
 
         private final CountDownLatch countDownLatch;
-        private Long pendingXid;
         private BatchState state;
 
         private final ExecutionList executionList = new ExecutionList();
 
         public InstallationFuture(Set<Dpid> sws, Map<Long, FlowRuleBatchEntry> fmXids) {
+            this.xid = xidCounter.getAndIncrement();
             this.state = BatchState.STARTED;
             this.sws = sws;
             this.fms = fmXids;
             countDownLatch = new CountDownLatch(sws.size());
         }
 
+        public Long xid() {
+            return xid;
+        }
+
         public void fail(OFErrorMsg msg, Dpid dpid) {
 
             ok.set(false);
@@ -385,27 +399,27 @@
                 case BAD_ACTION:
                     OFBadActionErrorMsg bad = (OFBadActionErrorMsg) msg;
                     fe = new DefaultFlowEntry(offending, bad.getErrType().ordinal(),
-                            bad.getCode().ordinal());
+                                              bad.getCode().ordinal());
                     break;
                 case BAD_INSTRUCTION:
                     OFBadInstructionErrorMsg badins = (OFBadInstructionErrorMsg) msg;
                     fe = new DefaultFlowEntry(offending, badins.getErrType().ordinal(),
-                            badins.getCode().ordinal());
+                                              badins.getCode().ordinal());
                     break;
                 case BAD_MATCH:
                     OFBadMatchErrorMsg badMatch = (OFBadMatchErrorMsg) msg;
                     fe = new DefaultFlowEntry(offending, badMatch.getErrType().ordinal(),
-                            badMatch.getCode().ordinal());
+                                              badMatch.getCode().ordinal());
                     break;
                 case BAD_REQUEST:
                     OFBadRequestErrorMsg badReq = (OFBadRequestErrorMsg) msg;
                     fe = new DefaultFlowEntry(offending, badReq.getErrType().ordinal(),
-                            badReq.getCode().ordinal());
+                                              badReq.getCode().ordinal());
                     break;
                 case FLOW_MOD_FAILED:
                     OFFlowModFailedErrorMsg fmFail = (OFFlowModFailedErrorMsg) msg;
                     fe = new DefaultFlowEntry(offending, fmFail.getErrType().ordinal(),
-                            fmFail.getCode().ordinal());
+                                              fmFail.getCode().ordinal());
                     break;
                 case EXPERIMENTER:
                 case GROUP_MOD_FAILED:
@@ -434,13 +448,12 @@
         }
 
 
-        public void verify(Long id) {
-            pendingXid = id;
+        public void verify() {
             for (Dpid dpid : sws) {
                 OpenFlowSwitch sw = controller.getSwitch(dpid);
                 OFBarrierRequest.Builder builder = sw.factory()
                         .buildBarrierRequest()
-                        .setXid(id);
+                        .setXid(xid);
                 sw.sendMsg(builder.build());
             }
         }
@@ -462,7 +475,6 @@
                 }
 
             }
-            invokeCallbacks();
             return true;
         }
 
@@ -481,6 +493,7 @@
             countDownLatch.await();
             this.state = BatchState.FINISHED;
             CompletedBatchOperation result = new CompletedBatchOperation(ok.get(), offendingFlowMods);
+            //FIXME do cleanup here
             return result;
         }
 
@@ -491,6 +504,7 @@
             if (countDownLatch.await(timeout, unit)) {
                 this.state = BatchState.FINISHED;
                 CompletedBatchOperation result = new CompletedBatchOperation(ok.get(), offendingFlowMods);
+                // FIXME do cleanup here
                 return result;
             }
             throw new TimeoutException();
@@ -498,9 +512,7 @@
 
         private void cleanUp() {
             if (isDone() || isCancelled()) {
-                if (pendingXid != null) {
-                    pendingFutures.remove(pendingXid);
-                }
+                pendingFutures.remove(xid);
                 for (Long xid : fms.keySet()) {
                     pendingFMs.remove(xid);
                 }
@@ -509,21 +521,10 @@
 
         private void removeRequirement(Dpid dpid) {
             countDownLatch.countDown();
-            if (countDownLatch.getCount() == 0) {
-                invokeCallbacks();
-            }
             sws.remove(dpid);
+            //FIXME don't do cleanup here
             cleanUp();
         }
-
-        @Override
-        public void addListener(Runnable runnable, Executor executor) {
-            executionList.add(runnable, executor);
-        }
-
-        private void invokeCallbacks() {
-            executionList.execute();
-        }
     }
 
 }
diff --git a/tools/package/config/README b/tools/package/config/README
index 62b758d..970f87a 100644
--- a/tools/package/config/README
+++ b/tools/package/config/README
@@ -1,2 +1,2 @@
-onos-config command will copy files contained in this directory to ONOS instances according to cell definition
-
+The onos-config command will copy files contained in this directory to ONOS
+instances according to cell definition.
diff --git a/tools/test/bin/onos b/tools/test/bin/onos
index a0c126c..b18bae8 100755
--- a/tools/test/bin/onos
+++ b/tools/test/bin/onos
@@ -10,4 +10,5 @@
 [ "$1" = "-w" ] && shift && onos-wait-for-start $1
 
 [ -n "$1" ] && OCI=$(find_node $1) && shift
+unset KARAF_HOME
 client -h $OCI -u karaf "$@" 2>/dev/null
diff --git a/tools/test/bin/onos-gui b/tools/test/bin/onos-gui
index a0c843d..517ed11 100755
--- a/tools/test/bin/onos-gui
+++ b/tools/test/bin/onos-gui
@@ -3,10 +3,7 @@
 # Launches ONOS GUI on the specified node.
 # -----------------------------------------------------------------------------
 
-[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
-. $ONOS_ROOT/tools/build/envDefaults
-
 host=${1:-$OCI}
 host=${host:-localhost}
 
-open http://$host:8181/onos/tvue
+open http://$host:8181/onos/ui
diff --git a/tools/test/bin/onos-show-cell b/tools/test/bin/onos-show-cell
index eedea9e..5aee338 100755
--- a/tools/test/bin/onos-show-cell
+++ b/tools/test/bin/onos-show-cell
@@ -40,6 +40,7 @@
 # Load the cell setup
 . $ONOS_ROOT/tools/test/cells/${cell}
 
+echo "ONOS_CELL=${ONOS_CELL}"
 echo "ONOS_NIC=${ONOS_NIC}"
 for n in {0..9}; do
     ocn="OC${n}"
diff --git a/tools/test/topos/oe-nonlinear-10.json b/tools/test/topos/oe-nonlinear-10.json
index f23bb9b..522215b 100644
--- a/tools/test/topos/oe-nonlinear-10.json
+++ b/tools/test/topos/oe-nonlinear-10.json
@@ -108,33 +108,26 @@
     ],
 
     "links" : [
-        { "src": "of:0000ffffffffff01/10", "dst": "of:0000ffffffffff02/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff02/10", "dst": "of:0000ffffffffff03/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff03/30", "dst": "of:0000ffffffffff04/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff02/20", "dst": "of:0000ffffffffff05/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff03/20", "dst": "of:0000ffffffffff06/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff05/30", "dst": "of:0000ffffffffff06/20", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff05/20", "dst": "of:0000ffffffffff07/21", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff06/30", "dst": "of:0000ffffffffff08/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff07/30", "dst": "of:0000ffffffffff08/20", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff07/20", "dst": "of:0000ffffffffff09/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
-        { "src": "of:0000ffffffffff08/30", "dst": "of:0000ffffffffff0A/10", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff01/50", "dst": "of:0000ffffffffff02/30","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff02/50", "dst": "of:0000ffffffffff03/30","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff03/50", "dst": "of:0000ffffffffff04/50","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff01/20", "dst": "of:0000ffffffffff05/50","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff02/20", "dst": "of:0000ffffffffff05/20","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff03/20", "dst": "of:0000ffffffffff06/50","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff04/20", "dst": "of:0000ffffffffff06/20","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff05/30", "dst": "of:0000ffffffffff06/40","type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff05/40", "dst": "of:0000ffffffffff07/50", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff06/30", "dst": "of:0000ffffffffff08/50", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff07/20", "dst": "of:0000ffffffffff08/30", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff07/30", "dst": "of:0000ffffffffff09/50", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
+        { "src": "of:0000ffffffffff08/20", "dst": "of:0000ffffffffff0A/50", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
         { "src": "of:0000ffffffffff09/20", "dst": "of:0000ffffffffff0A/20", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "optical.kms": 1000, "durable": "true" } },
 
-        { "src": "of:0000ffffffff0001/2", "dst": "of:0000ffffffffff01/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
-        { "src": "of:0000ffffffff0003/2", "dst": "of:0000ffffffffff03/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
-        { "src": "of:0000ffffffff0004/2", "dst": "of:0000ffffffffff04/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
-        { "src": "of:0000ffffffff0007/2", "dst": "of:0000ffffffffff07/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
-        { "src": "of:0000ffffffff0009/2", "dst": "of:0000ffffffffff09/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
-        { "src": "of:0000ffffffff000A/2", "dst": "of:0000ffffffffff0A/1", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } }
-    ],
-
-    "hosts" : [
-        { "mac": "00:00:00:00:00:01", "vlan": -1, "location": "of:0000ffffffff0001/1", "ip": "10.0.0.1" },
-        { "mac": "00:00:00:00:00:03", "vlan": -1, "location": "of:0000ffffffff0003/1", "ip": "10.0.0.3" },
-        { "mac": "00:00:00:00:00:04", "vlan": -1, "location": "of:0000ffffffff0004/1", "ip": "10.0.0.4" },
-        { "mac": "00:00:00:00:00:07", "vlan": -1, "location": "of:0000ffffffff0007/1", "ip": "10.0.0.7" },
-        { "mac": "00:00:00:00:00:09", "vlan": -1, "location": "of:0000ffffffff0009/1", "ip": "10.0.0.9" },
-        { "mac": "00:00:00:00:00:0A", "vlan": -1, "location": "of:0000ffffffff000A/1", "ip": "10.0.0.10" }
+        { "src": "of:0000ffffffff0001/2", "dst": "of:0000ffffffffff01/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0002/2", "dst": "of:0000ffffffffff04/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0003/2", "dst": "of:0000ffffffffff06/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0004/2", "dst": "of:0000ffffffffff07/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0005/2", "dst": "of:0000ffffffffff09/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0006/2", "dst": "of:0000ffffffffff0A/10",  "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } }
     ]
-}
\ No newline at end of file
+}
diff --git a/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
index 2ac20b7..421a3e9 100644
--- a/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
+++ b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
@@ -43,9 +43,9 @@
      * @param clazz the class to check
      * @return true if the given class is a properly specified immutable class.
      */
-    private boolean isImmutableClass(Class<?> clazz) {
+    private boolean isImmutableClass(Class<?> clazz, boolean allowNonFinalClass) {
         // class must be declared final
-        if (!Modifier.isFinal(clazz.getModifiers())) {
+        if (!allowNonFinalClass && !Modifier.isFinal(clazz.getModifiers())) {
             failureReason = "a class that is not final";
             return false;
         }
@@ -113,16 +113,16 @@
     }
 
     /**
-     * Assert that the given class adheres to the utility class rules.
+     * Assert that the given class adheres to the immutable class rules.
      *
      * @param clazz the class to check
      *
-     * @throws java.lang.AssertionError if the class is not a valid
-     *         utility class
+     * @throws java.lang.AssertionError if the class is not an
+     *         immutable class
      */
     public static void assertThatClassIsImmutable(Class<?> clazz) {
         final ImmutableClassChecker checker = new ImmutableClassChecker();
-        if (!checker.isImmutableClass(clazz)) {
+        if (!checker.isImmutableClass(clazz, false)) {
             final Description toDescription = new StringDescription();
             final Description mismatchDescription = new StringDescription();
 
@@ -136,4 +136,31 @@
             throw new AssertionError(reason);
         }
     }
+
+    /**
+     * Assert that the given class adheres to the immutable class rules, but
+     * is not declared final.  Classes that need to be inherited from cannot be
+     * declared final.
+     *
+     * @param clazz the class to check
+     *
+     * @throws java.lang.AssertionError if the class is not an
+     *         immutable class
+     */
+    public static void assertThatClassIsImmutableBaseClass(Class<?> clazz) {
+        final ImmutableClassChecker checker = new ImmutableClassChecker();
+        if (!checker.isImmutableClass(clazz, true)) {
+            final Description toDescription = new StringDescription();
+            final Description mismatchDescription = new StringDescription();
+
+            checker.describeTo(toDescription);
+            checker.describeMismatch(mismatchDescription);
+            final String reason =
+                    "\n" +
+                            "Expected: is \"" + toDescription.toString() + "\"\n" +
+                            "    but : was \"" + mismatchDescription.toString() + "\"";
+
+            throw new AssertionError(reason);
+        }
+    }
 }
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyMessages.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyMessages.java
index 0b54dd8..f747423 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyMessages.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyMessages.java
@@ -24,6 +24,7 @@
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.mastership.MastershipService;
+import org.onlab.onos.net.Annotated;
 import org.onlab.onos.net.Annotations;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultEdgeLink;
@@ -45,6 +46,8 @@
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpAddress;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Iterator;
 import java.util.Map;
@@ -68,6 +71,8 @@
  */
 public abstract class TopologyMessages {
 
+    protected static final Logger log = LoggerFactory.getLogger(TopologyMessages.class);
+
     private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.core", true);
     private static final String COMPACT = "%s/%s-%s/%s";
 
@@ -195,7 +200,7 @@
                 .put("id", device.id().toString())
                 .put("type", device.type().toString().toLowerCase())
                 .put("online", deviceService.isAvailable(device.id()))
-                .put("master", mastershipService.getMasterFor(device.id()).toString());
+                .put("master", master(device.id()));
 
         // Generate labels: id, chassis id, no-label, optional-name
         ArrayNode labels = mapper.createArrayNode();
@@ -207,6 +212,7 @@
         // Add labels, props and stuff the payload into envelope.
         payload.set("labels", labels);
         payload.set("props", props(device.annotations()));
+        addGeoLocation(device, payload);
         addMetaUi(device.id().toString(), payload);
 
         String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
@@ -220,6 +226,7 @@
         ObjectNode payload = mapper.createObjectNode()
                 .put("id", compactLinkString(link))
                 .put("type", link.type().toString().toLowerCase())
+                .put("online", true) // TODO: add link state field
                 .put("linkWidth", 2)
                 .put("src", link.src().deviceId().toString())
                 .put("srcPort", link.src().port().toString())
@@ -237,10 +244,11 @@
                 .put("id", host.id().toString())
                 .put("ingress", compactLinkString(edgeLink(host, true)))
                 .put("egress", compactLinkString(edgeLink(host, false)));
-        payload.set("cp", location(mapper, host.location()));
+        payload.set("cp", hostConnect(mapper, host.location()));
         payload.set("labels", labels(mapper, ip(host.ipAddresses()),
                                      host.mac().toString()));
         payload.set("props", props(host.annotations()));
+        addGeoLocation(host, payload);
         addMetaUi(host.id().toString(), payload);
 
         String type = (event.type() == HOST_ADDED) ? "addHost" :
@@ -249,7 +257,7 @@
     }
 
     // Encodes the specified host location into a JSON object.
-    private ObjectNode location(ObjectMapper mapper, HostLocation location) {
+    private ObjectNode hostConnect(ObjectMapper mapper, HostLocation location) {
         return mapper.createObjectNode()
                 .put("device", location.deviceId().toString())
                 .put("port", location.port().toLong());
@@ -264,6 +272,12 @@
         return json;
     }
 
+    // Returns the name of the master node for the specified device id.
+    private String master(DeviceId deviceId) {
+        NodeId master = mastershipService.getMasterFor(deviceId);
+        return master != null ? master.toString() : "";
+    }
+
     // Generates an edge link from the specified host location.
     private EdgeLink edgeLink(Host host, boolean ingress) {
         return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)),
@@ -278,10 +292,28 @@
         }
     }
 
+    // Adds a geo location JSON to the specified payload object.
+    private void addGeoLocation(Annotated annotated, ObjectNode payload) {
+        Annotations annotations = annotated.annotations();
+        String slat = annotations.value("latitude");
+        String slng = annotations.value("longitude");
+        try {
+            if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) {
+                double lat = Double.parseDouble(slat);
+                double lng = Double.parseDouble(slng);
+                ObjectNode loc = mapper.createObjectNode()
+                        .put("type", "latlng").put("lat", lat).put("lng", lng);
+                payload.set("location", loc);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("Invalid geo data latitude={}; longiture={}", slat, slng);
+        }
+    }
+
     // Updates meta UI information for the specified object.
     protected void updateMetaUi(ObjectNode event) {
         ObjectNode payload = payload(event);
-        metaUi.put(string(payload, "id"), payload);
+        metaUi.put(string(payload, "id"), (ObjectNode) payload.path("memento"));
     }
 
     // Returns device details response.
@@ -289,7 +321,6 @@
         Device device = deviceService.getDevice(deviceId);
         Annotations annot = device.annotations();
         int portCount = deviceService.getPorts(deviceId).size();
-        NodeId master = mastershipService.getMasterFor(device.id());
         return envelope("showDetails", sid,
                         json(deviceId.toString(),
                              device.type().toString().toLowerCase(),
@@ -303,7 +334,7 @@
                              new Prop("Longitude", annot.value("longitude")),
                              new Prop("Ports", Integer.toString(portCount)),
                              new Separator(),
-                             new Prop("Master", master.toString())));
+                             new Prop("Master", master(deviceId))));
     }
 
     // Returns host details response.
@@ -319,16 +350,15 @@
                              new Prop("Longitude", annot.value("longitude"))));
     }
 
-
     // Produces a path message to the client.
-    protected ObjectNode pathMessage(Path path) {
+    protected ObjectNode pathMessage(Path path, String type) {
         ObjectNode payload = mapper.createObjectNode();
         ArrayNode links = mapper.createArrayNode();
         for (Link link : path.links()) {
             links.add(compactLinkString(link));
         }
 
-        payload.set("links", links);
+        payload.put("type", type).set("links", links);
         return payload;
     }
 
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
index 83e54b4..2506b13 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -110,8 +110,8 @@
         try {
             ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
             String type = string(event, "event", "unknown");
-            if (type.equals("showDetails")) {
-                showDetails(event);
+            if (type.equals("requestDetails")) {
+                requestDetails(event);
             } else if (type.equals("updateMeta")) {
                 updateMetaUi(event);
             } else if (type.equals("requestPath")) {
@@ -122,7 +122,7 @@
                 cancelTraffic(event);
             }
         } catch (Exception e) {
-            System.out.println("WTF?! " + data);
+            log.warn("Unable to parse GUI request {} due to {}", data, e);
             e.printStackTrace();
         }
     }
@@ -165,9 +165,9 @@
     }
 
     // Sends back device or host details.
-    private void showDetails(ObjectNode event) {
+    private void requestDetails(ObjectNode event) {
         ObjectNode payload = payload(event);
-        String type = string(payload, "type", "unknown");
+        String type = string(payload, "class", "unknown");
         if (type.equals("device")) {
             sendMessage(deviceDetails(deviceId(string(payload, "id")),
                                       number(event, "sid")));
@@ -282,7 +282,8 @@
                 if (installable != null && !installable.isEmpty()) {
                     PathIntent pathIntent = (PathIntent) installable.iterator().next();
                     Path path = pathIntent.path();
-                    ObjectNode payload = pathMessage(path).put("intentId", intent.id().toString());
+                    ObjectNode payload = pathMessage(path, "host")
+                            .put("intentId", intent.id().toString());
                     sendMessage(envelope("showPath", sid, payload));
                 }
             }
diff --git a/web/gui/src/main/webapp/WEB-INF/web.xml b/web/gui/src/main/webapp/WEB-INF/web.xml
index ab7a550..32f1ddd 100644
--- a/web/gui/src/main/webapp/WEB-INF/web.xml
+++ b/web/gui/src/main/webapp/WEB-INF/web.xml
@@ -21,7 +21,7 @@
     <display-name>ONOS GUI</display-name>
 
     <welcome-file-list>
-        <welcome-file>index.html</welcome-file>
+        <welcome-file>index2.html</welcome-file>
     </welcome-file-list>
 
     <servlet>
diff --git a/web/gui/src/main/webapp/floatPanel.css b/web/gui/src/main/webapp/floatPanel.css
new file mode 100644
index 0000000..1c5a815
--- /dev/null
+++ b/web/gui/src/main/webapp/floatPanel.css
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed 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.
+ */
+
+/*
+ ONOS GUI -- Floating Panels -- CSS file
+
+ @author Simon Hunt
+ */
+
+.fpanel {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 10%;
+    width: 280px;
+    right: -300px;
+    opacity: 0;
+    background-color: rgba(255,255,255,0.8);
+
+    padding: 10px;
+    color: black;
+    font-size: 10pt;
+    box-shadow: 2px 2px 16px #777;
+}
+
+/* TODO: light/dark themes */
+.light .fpanel {
+
+}
+.dark .fpanel {
+
+}
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 03272e9..ca5b6a8 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -40,6 +40,7 @@
     <link rel="stylesheet" href="base.css">
     <link rel="stylesheet" href="onos2.css">
     <link rel="stylesheet" href="mast2.css">
+    <link rel="stylesheet" href="floatPanel.css">
 
     <!-- This is where contributed stylesheets get INJECTED -->
     <!-- TODO: replace with template marker and inject refs server-side -->
@@ -62,8 +63,9 @@
         <div id="view">
             <!-- NOTE: views injected here by onos.js -->
         </div>
-        <div id="overlays">
-            <!-- NOTE: overlays injected here, as needed -->
+        <div id="floatPanels">
+            <!-- NOTE: floating panels injected here, as needed -->
+            <!--       see onos.ui.addFloatingPanel             -->
         </div>
         <div id="alerts">
             <!-- NOTE: alert content injected here, as needed -->
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex1.json b/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex1.json
index f00cf2c..b82fda8 100644
--- a/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex1.json
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex1.json
@@ -10,6 +10,10 @@
       "",
       null
     ],
-    "props": {}
+    "props": {
+      "latitude": 123.5,
+      "longitude": 67.8,
+      "anotherProp": "foobar"
+    }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex2_memo.json b/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex2_memo.json
new file mode 100644
index 0000000..5f519ff
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/addDevice_ex2_memo.json
@@ -0,0 +1,24 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000000000000003",
+    "type": "switch",
+    "online": true,
+    "labels": [
+      "of:0000000000000003",
+      "3",
+      "",
+      null
+    ],
+    "props": {
+      "latitude": 123.5,
+      "longitude": 67.8,
+      "anotherProp": "foobar"
+    },
+    "metaUi": {
+      "xpc": 57.3,
+      "ypc": 24.86,
+      "and": "other properties the UI wishes to remember..."
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
new file mode 100644
index 0000000..19d9959
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex1_host.json
@@ -0,0 +1,22 @@
+{
+  "event": "showDetails",
+  "sid": 9,
+  "payload": {
+    "id": "CA:4B:EE:A4:B0:33/-1",
+    "type": "host",
+    "propOrder": [
+      "MAC",
+      "IP",
+      "-",
+      "Latitude",
+      "Longitude"
+    ],
+    "props": {
+      "MAC": "CA:4B:EE:A4:B0:33",
+      "IP": "[10.0.0.1]",
+      "-": "",
+      "Latitude": null,
+      "Longitude": null
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
new file mode 100644
index 0000000..8ac1f4f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showDetails_ex2_device.json
@@ -0,0 +1,33 @@
+{
+  "event": "showDetails",
+  "sid": 37,
+  "payload": {
+    "id": "of:000000000000000a",
+    "type": "switch",
+    "propOrder": [
+      "Name",
+      "Vendor",
+      "H/W Version",
+      "S/W Version",
+      "Serial Number",
+      "-",
+      "Latitude",
+      "Longitude",
+      "Ports",
+      "-",
+      "Master"
+    ],
+    "props": {
+      "Name": null,
+      "Vendor": "Nicira, Inc.",
+      "H/W Version": "Open vSwitch",
+      "S/W Version": "2.0.1",
+      "Serial Number": "None",
+      "-": "",
+      "Latitude": null,
+      "Longitude": null,
+      "Ports": "5",
+      "Master":"local"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
new file mode 100644
index 0000000..6c88605
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex1.json
@@ -0,0 +1,9 @@
+{
+  "event": "requestDetails",
+  "sid": 15,
+  "payload": {
+    "id": "of:0000000000000003",
+    "class": "device"
+  }
+}
+
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
new file mode 100644
index 0000000..2cc1bfa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestDetails_ex2.json
@@ -0,0 +1,8 @@
+{
+  "event": "requestDetails",
+  "sid": 9,
+  "payload": {
+    "id": "CA:4B:EE:A4:B0:33/-1",
+    "class": "host"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/updateMeta_ex1.json b/web/gui/src/main/webapp/json/ev/_capture/tx/updateMeta_ex1.json
index c04727e..6114583 100644
--- a/web/gui/src/main/webapp/json/ev/_capture/tx/updateMeta_ex1.json
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/updateMeta_ex1.json
@@ -4,7 +4,11 @@
   "payload": {
     "id": "62:4F:65:BF:FF:B3/-1",
     "class": "host",
-    "x": 197,
-    "y": 177
+    "memento": {
+      "xpc": 57.3,
+      "ypc": 24.86,
+      "and": "other properties the UI wishes to remember..."
+    }
   }
 }
+
diff --git a/web/gui/src/main/webapp/json/ev/intentSketch/scenario.json b/web/gui/src/main/webapp/json/ev/intentSketch/scenario.json
index 136d027..f109dde 100644
--- a/web/gui/src/main/webapp/json/ev/intentSketch/scenario.json
+++ b/web/gui/src/main/webapp/json/ev/intentSketch/scenario.json
@@ -5,5 +5,9 @@
   "title": "Host Intent Scenario",
   "params": {
     "lastAuto": 0
-  }
+  },
+  "description": [
+    "Currently this is just a sketch of the event sequence,",
+    " but is NOT YET a runnable scenario."
+  ]
 }
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_13_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_13_onos.json
new file mode 100644
index 0000000..5320841
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_13_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "removeHost",
+  "payload": {
+    "id": "A6:96:E5:03:52:5F/-1",
+    "ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
+    "egress": "of:0000ffffffff0008/1-A6:96:E5:03:52:5F/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff0008",
+      "port": 1
+    },
+    "labels": [
+      "10.0.0.17",
+      "A6:96:E5:03:52:5F"
+    ],
+    "props": {}
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json b/web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json
deleted file mode 100644
index 9d6e737..0000000
--- a/web/gui/src/main/webapp/json/ev/simple/ev_13_ui.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "event": "noop",
-  "payload": {
-    "id": "xyyzy"
-  }
-}
diff --git a/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
index 8656a90..fa7ae6e 100644
--- a/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
+++ b/web/gui/src/main/webapp/json/ev/simple/ev_1_onos.json
@@ -4,6 +4,11 @@
     "id": "of:0000ffffffff0008",
     "type": "switch",
     "online": false,
+    "location": {
+      "type": "latlng",
+      "lat": 37.6,
+      "lng": 122.3
+    },
     "labels": [
       "0000ffffffff0008",
       "FF:FF:FF:FF:00:08",
diff --git a/web/gui/src/main/webapp/json/ev/simple/scenario.json b/web/gui/src/main/webapp/json/ev/simple/scenario.json
index 5fb8869..4c55b2d 100644
--- a/web/gui/src/main/webapp/json/ev/simple/scenario.json
+++ b/web/gui/src/main/webapp/json/ev/simple/scenario.json
@@ -20,6 +20,6 @@
     "10. update link (increase width, update props)",
     "11. update link (reduce width, update props)",
     "12. remove link",
-    ""
+    "13. remove host (10.0.0.17)"
   ]
-}
\ No newline at end of file
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json
index 9b30b4a..ec0f258 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_10_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff04",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff04",
       "FF:FF:FF:FF:FF:04",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json
index 3b361d5..04e6754 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_11_onos.json
@@ -3,15 +3,15 @@
   "payload": {
     "id": "of:0000ffffffff000A",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff000A",
       "FF:FF:FF:FF:00:0A",
       "?"
     ],
     "metaUi": {
-      "Zx": 832,
-      "Zy": 223
+      "x": 832,
+      "y": 223
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json
index e53a6cd..c778cd5 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_12_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffff0001",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff0001",
       "FF:FF:FF:FF:00:01",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json
index 6d2341f..a0cc21f 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_13_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff01",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff01",
       "FF:FF:FF:FF:FF:01",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json
index e196148..93127a8 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_14_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffff0004",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff0004",
       "FF:FF:FF:FF:00:04",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json
index 30ba9f3..f2d6891 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_15_onos.json
@@ -3,15 +3,15 @@
   "payload": {
     "id": "of:0000ffffffffff0A",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff0A",
       "FF:FF:FF:FF:FF:0A",
       "?"
     ],
     "metaUi": {
-      "Zx": 840,
-      "Zy": 290
+      "x": 840,
+      "y": 290
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json
index 274adc1..3b0db4b 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_16_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff09",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff09",
       "FF:FF:FF:FF:FF:09",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json
index 82272a4..69c62b9 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_17_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff02/20-of:0000ffffffffff05/10",
+    "type": "optical",
+    "linkWidth": 4,
     "src": "of:0000ffffffffff02",
     "srcPort": "20",
     "dst": "of:0000ffffffffff05",
     "dstPort": "10",
-    "type": "optical",
-    "linkWidth": 6,
     "props" : {
       "BW": "80 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json
index 5687698..d11f13e 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_18_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffff000A/2-of:0000ffffffffff0A/1",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffff000A",
     "srcPort": "2",
     "dst": "of:0000ffffffffff0A",
     "dstPort": "1",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "100 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json
index 24aeb2d..1349a3b 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_19_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff03/10-of:0000ffffffffff02/10",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff03",
     "srcPort": "10",
     "dst": "of:0000ffffffffff02",
     "dstPort": "10",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json
index d4c8ddb..00a3e17 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_1_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff08",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff08",
       "FF:FF:FF:FF:FF:08",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json
index f42b50e..e4d2161 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_20_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff07/21-of:0000ffffffffff05/20",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff07",
     "srcPort": "21",
     "dst": "of:0000ffffffffff05",
     "dstPort": "20",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json
index 5af0ac7..ccdad88 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_21_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffff0001/2-of:0000ffffffffff01/1",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffff0001",
     "srcPort": "2",
     "dst": "of:0000ffffffffff01",
     "dstPort": "1",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json
index 0d4cf2b..52a4a02 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_22_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff09/20-of:0000ffffffffff0A/20",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff09",
     "srcPort": "20",
     "dst": "of:0000ffffffffff0A",
     "dstPort": "20",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json
index fff0f2b..be778551 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_23_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
-    "src": "of:0000ffffffffff06",
-    "srcPort": "20",
-    "dst": "of:0000ffffffffff05",
-    "dstPort": "30",
+    "id": "of:0000ffffffffff07/30-of:0000ffffffffff08/20",
     "type": "optical",
-    "linkWidth": 6,
+    "linkWidth": 4,
+    "src": "of:0000ffffffffff07",
+    "srcPort": "30",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "20",
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json
index 756b6c1..6ae5192 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_24_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
-    "src": "of:0000ffffffffff07",
-    "srcPort": "30",
-    "dst": "of:0000ffffffffff08",
-    "dstPort": "20",
+    "id": "of:0000ffffffffff02/10-of:0000ffffffffff01/10",
     "type": "optical",
-    "linkWidth": 6,
+    "linkWidth": 2,
+    "src": "of:0000ffffffffff02",
+    "srcPort": "10",
+    "dst": "of:0000ffffffffff01",
+    "dstPort": "10",
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json
index adad8a6..7fabce6 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_25_onos.json
@@ -1,14 +1,15 @@
 {
   "event": "addLink",
   "payload": {
-    "src": "of:0000ffffffffff03",
-    "srcPort": "20",
-    "dst": "of:0000ffffffffff06",
+    "id": "of:0000ffffffffff04/27-of:0000ffffffffff08/10",
+    "src": "of:0000ffffffffff04",
+    "srcPort": "27",
+    "dst": "of:0000ffffffffff08",
     "dstPort": "10",
     "type": "optical",
     "linkWidth": 2,
     "props" : {
-      "BW": "70 G"
+      "BW": "30 G"
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json
index 245c823..b89a287 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_26_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
-    "src": "of:0000ffffffffff02",
-    "srcPort": "10",
-    "dst": "of:0000ffffffffff01",
-    "dstPort": "10",
+    "id": "of:0000ffffffff0003/2-of:0000ffffffffff03/1",
     "type": "optical",
     "linkWidth": 2,
+    "src": "of:0000ffffffff0003",
+    "srcPort": "2",
+    "dst": "of:0000ffffffffff03",
+    "dstPort": "1",
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json
index b856573..112a33e 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_27_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff09/1-of:0000ffffffff0009/2",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff09",
     "srcPort": "1",
     "dst": "of:0000ffffffff0009",
     "dstPort": "2",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json
index 232dc3b..52207df 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_28_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff03/30-of:0000ffffffffff04/10",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff03",
     "srcPort": "30",
     "dst": "of:0000ffffffffff04",
     "dstPort": "10",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json
index 1a845ce..c4660a3 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_29_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff07/20-of:0000ffffffffff09/10",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff07",
     "srcPort": "20",
     "dst": "of:0000ffffffffff09",
     "dstPort": "10",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json
index fd446ba..8d4fbfa 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_2_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff03",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff03",
       "FF:FF:FF:FF:FF:03",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json
index a617f45..e23cc2a 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_30_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff0A/10-of:0000ffffffffff08/30",
+    "type": "optical",
+    "linkWidth": 4,
     "src": "of:0000ffffffffff0A",
     "srcPort": "10",
     "dst": "of:0000ffffffffff08",
     "dstPort": "30",
-    "type": "optical",
-    "linkWidth": 6,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json
index 438aa1b..a798e53 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_31_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffff0004/2-of:0000ffffffffff04/1",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffff0004",
     "srcPort": "2",
     "dst": "of:0000ffffffffff04",
     "dstPort": "1",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json
index c479f01..aac204f 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_32_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff07/1-of:0000ffffffff0007/2",
+    "type": "optical",
+    "linkWidth": 2,
     "src": "of:0000ffffffffff07",
     "srcPort": "1",
     "dst": "of:0000ffffffff0007",
     "dstPort": "2",
-    "type": "optical",
-    "linkWidth": 2,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json
index 2cc3a32..ab23e93 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_33_onos.json
@@ -1,14 +1,17 @@
 {
-  "event": "addLink",
+  "event": "updateDevice",
   "payload": {
-    "src": "of:0000ffffffff0003",
-    "srcPort": "2",
-    "dst": "of:0000ffffffffff03",
-    "dstPort": "1",
-    "type": "optical",
-    "linkWidth": 2,
-    "props" : {
-      "BW": "70 G"
+    "id": "of:0000ffffffffff06",
+    "type": "roadm",
+    "online": true,
+    "labels": [
+      "0000ffffffffff06",
+      "FF:FF:FF:FF:FF:06",
+      "?"
+    ],
+    "metaUi": {
+      "x": 336,
+      "y": 254
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json
index fa5e3bc..09be8e6 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_34_onos.json
@@ -1,12 +1,13 @@
 {
   "event": "addLink",
   "payload": {
+    "id": "of:0000ffffffffff06/20-of:0000ffffffffff05/30",
     "src": "of:0000ffffffffff06",
-    "srcPort": "30",
-    "dst": "of:0000ffffffffff08",
-    "dstPort": "10",
+    "srcPort": "20",
+    "dst": "of:0000ffffffffff05",
+    "dstPort": "30",
     "type": "optical",
-    "linkWidth": 6,
+    "linkWidth": 4,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json
index c579e59..4b612e9 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_35_onos.json
@@ -1,14 +1,15 @@
 {
   "event": "addLink",
   "payload": {
-    "src": "of:0000ffffffffff04",
-    "srcPort": "27",
-    "dst": "of:0000ffffffffff08",
-    "dstPort": "10",
+    "id": "of:0000ffffffffff03/20-of:0000ffffffffff06/10",
     "type": "optical",
     "linkWidth": 2,
+    "src": "of:0000ffffffffff03",
+    "srcPort": "20",
+    "dst": "of:0000ffffffffff06",
+    "dstPort": "10",
     "props" : {
-      "BW": "30 G"
+      "BW": "70 G"
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_36_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_36_onos.json
new file mode 100644
index 0000000..cddb929
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_36_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffffff06/30-of:0000ffffffffff08/10",
+    "type": "optical",
+    "linkWidth": 4,
+    "src": "of:0000ffffffffff06",
+    "srcPort": "30",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "10",
+    "props" : {
+      "BW": "70 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_37_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_37_onos.json
new file mode 100644
index 0000000..6f608cd
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_37_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "updateDevice",
+  "payload": {
+    "id": "of:0000ffffffffff08",
+    "type": "roadm",
+    "online": false,
+    "labels": [
+      "0000ffffffffff08",
+      "FF:FF:FF:FF:FF:08",
+      "?"
+    ],
+    "metaUi": {
+      "x": 539,
+      "y": 186
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_38_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_38_onos.json
new file mode 100644
index 0000000..09a0339
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_38_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "removeLink",
+  "payload": {
+    "id": "of:0000ffffffffff07/30-of:0000ffffffffff08/20",
+    "type": "optical",
+    "linkWidth": 4,
+    "src": "of:0000ffffffffff07",
+    "srcPort": "30",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "20",
+    "props" : {
+      "BW": "70 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_39_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_39_onos.json
new file mode 100644
index 0000000..a85cee6
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_39_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "removeLink",
+  "payload": {
+    "id": "of:0000ffffffffff04/27-of:0000ffffffffff08/10",
+    "src": "of:0000ffffffffff04",
+    "srcPort": "27",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "10",
+    "type": "optical",
+    "linkWidth": 2,
+    "props" : {
+      "BW": "30 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json
index 23bf26a..b4c3537 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_3_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffff0007",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff0007",
       "FF:FF:FF:FF:00:07",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_40_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_40_onos.json
new file mode 100644
index 0000000..1b95d24
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_40_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "removeLink",
+  "payload": {
+    "id": "of:0000ffffffffff0A/10-of:0000ffffffffff08/30",
+    "type": "optical",
+    "linkWidth": 4,
+    "src": "of:0000ffffffffff0A",
+    "srcPort": "10",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "30",
+    "props" : {
+      "BW": "70 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_41_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_41_onos.json
new file mode 100644
index 0000000..1efc1f6
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_41_onos.json
@@ -0,0 +1,15 @@
+{
+  "event": "removeLink",
+  "payload": {
+    "id": "of:0000ffffffffff06/30-of:0000ffffffffff08/10",
+    "type": "optical",
+    "linkWidth": 4,
+    "src": "of:0000ffffffffff06",
+    "srcPort": "30",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "10",
+    "props" : {
+      "BW": "70 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json
index c600401..053b963 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_4_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffff0009",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff0009",
       "FF:FF:FF:FF:00:09",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json
index af912a7..332bfdb 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_5_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff02",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff02",
       "FF:FF:FF:FF:FF:02",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json
index 50273bc..c764bc1 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_6_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffff0003",
     "type": "switch",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffff0003",
       "FF:FF:FF:FF:00:03",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json
index 7cd29b0..25a6dce 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_7_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff07",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff07",
       "FF:FF:FF:FF:FF:07",
diff --git a/web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json b/web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json
index 1e0f427..0f497c0 100644
--- a/web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json
+++ b/web/gui/src/main/webapp/json/ev/startup/ev_9_onos.json
@@ -3,7 +3,7 @@
   "payload": {
     "id": "of:0000ffffffffff05",
     "type": "roadm",
-    "online": false,
+    "online": true,
     "labels": [
       "0000ffffffffff05",
       "FF:FF:FF:FF:FF:05",
diff --git a/web/gui/src/main/webapp/json/ev/startup/scenario.json b/web/gui/src/main/webapp/json/ev/startup/scenario.json
index 37939ca0..089d53a 100644
--- a/web/gui/src/main/webapp/json/ev/startup/scenario.json
+++ b/web/gui/src/main/webapp/json/ev/startup/scenario.json
@@ -6,5 +6,15 @@
   "title": "Startup Scenario",
   "params": {
     "lastAuto": 32
-  }
+  },
+  "description": [
+    "Loads 16 devices (10 optical, 6 packet)",
+    " and their associated links.",
+    "",
+    "Press 'S' to load initial events.",
+    "",
+    "Press spacebar to complete the scenario...",
+    " * 4 events - device online, add 3 links",
+    " * 5 events - device offline, remove 4 links"
+  ]
 }
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/json/of_0000000000000001.json b/web/gui/src/main/webapp/json/of_0000000000000001.json
index 719af80..1f5c8e9 100644
--- a/web/gui/src/main/webapp/json/of_0000000000000001.json
+++ b/web/gui/src/main/webapp/json/of_0000000000000001.json
@@ -3,6 +3,11 @@
     "id": "of:0000000000000001",
     "type": "roadm",
     "propOrder": [ "name", "type", "-", "dpid", "latitude", "longitude", "allowed" ],
+    "location": {
+        "type": "latlng",
+        "lat": 37.6,
+        "lng": 122.3
+    },
     "props": {
         "allowed": true,
         "latitude": 37.6,
diff --git a/web/gui/src/main/webapp/json/of_0000000000000002.json b/web/gui/src/main/webapp/json/of_0000000000000002.json
index a6b0e7c..87fd1f2 100644
--- a/web/gui/src/main/webapp/json/of_0000000000000002.json
+++ b/web/gui/src/main/webapp/json/of_0000000000000002.json
@@ -3,6 +3,11 @@
     "id": "of:0000000000000002",
     "type": "switch",
     "propOrder": [ "name", "type", "dpid", "latitude", "longitude", "allowed" ],
+    "location": {
+        "type": "latlng",
+        "lat": 37.6,
+        "lng": 122.3
+    },
     "props": {
         "allowed": true,
         "latitude": 37.3,
diff --git a/web/gui/src/main/webapp/json/of_0000000000000003.json b/web/gui/src/main/webapp/json/of_0000000000000003.json
index 9fd2790..1315961 100644
--- a/web/gui/src/main/webapp/json/of_0000000000000003.json
+++ b/web/gui/src/main/webapp/json/of_0000000000000003.json
@@ -3,6 +3,11 @@
     "id": "of:0000000000000003",
     "type": "switch",
     "propOrder": [ "name", "type", "dpid", "latitude", "longitude", "allowed" ],
+    "location": {
+        "type": "latlng",
+        "lat": 33.9,
+        "lng": 118.4
+    },
     "props": {
         "allowed": true,
         "latitude": 33.9,
diff --git a/web/gui/src/main/webapp/json/of_0000000000000004.json b/web/gui/src/main/webapp/json/of_0000000000000004.json
index f3f2132..ba243baf 100644
--- a/web/gui/src/main/webapp/json/of_0000000000000004.json
+++ b/web/gui/src/main/webapp/json/of_0000000000000004.json
@@ -3,6 +3,11 @@
     "id": "of:0000000000000004",
     "type": "switch",
     "propOrder": [ "name", "type", "dpid", "latitude", "longitude", "allowed" ],
+    "location": {
+        "type": "latlng",
+        "lat": 32.8,
+        "lng": 117.1
+    },
     "props": {
         "allowed": true,
         "latitude": 32.8,
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index f38b35f..0644c32 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -50,6 +50,7 @@
 
         // internal state
         var views = {},
+            fpanels = {},
             current = {
                 view: null,
                 ctx: '',
@@ -57,7 +58,7 @@
                 theme: settings.theme
             },
             built = false,
-            errorCount = 0,
+            buildErrors = [],
             keyHandler = {
                 globalKeys: {},
                 maskedKeys: {},
@@ -70,7 +71,11 @@
             };
 
         // DOM elements etc.
-        var $view,
+        // TODO: verify existence of following elements...
+        var $view = d3.select('#view'),
+            $floatPanels = d3.select('#floatPanels'),
+            $alerts = d3.select('#alerts'),
+            // note, following elements added programmatically...
             $mastRadio;
 
 
@@ -241,10 +246,22 @@
             setView(view, hash, t);
         }
 
+        function buildError(msg) {
+            buildErrors.push(msg);
+        }
+
         function reportBuildErrors() {
             traceFn('reportBuildErrors');
-            // TODO: validate registered views / nav-item linkage etc.
-            console.log('(no build errors)');
+            var nerr = buildErrors.length,
+                errmsg;
+            if (!nerr) {
+                console.log('(no build errors)');
+            } else {
+                errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
+                    buildErrors.join('\n');
+                doAlert(errmsg);
+                console.error(errmsg);
+            }
         }
 
         // returns the reference if it is a function, null otherwise
@@ -449,22 +466,20 @@
         }
 
         function createAlerts() {
-            var al = d3.select('#alerts')
-                .style('display', 'block');
-            al.append('span')
+            $alerts.style('display', 'block');
+            $alerts.append('span')
                 .attr('class', 'close')
                 .text('X')
                 .on('click', closeAlerts);
-            al.append('pre');
-            al.append('p').attr('class', 'footnote')
+            $alerts.append('pre');
+            $alerts.append('p').attr('class', 'footnote')
                 .text('Press ESCAPE to close');
             alerts.open = true;
             alerts.count = 0;
         }
 
         function closeAlerts() {
-            d3.select('#alerts')
-                .style('display', 'none')
+            $alerts.style('display', 'none')
                 .html('');
             alerts.open = false;
         }
@@ -474,7 +489,7 @@
                 oldContent;
 
             if (alerts.count) {
-                oldContent = d3.select('#alerts pre').html();
+                oldContent = $alerts.select('pre').html();
             }
 
             lines = msg.split('\n');
@@ -485,7 +500,7 @@
                 lines += '\n----\n' + oldContent;
             }
 
-            d3.select('#alerts pre').html(lines);
+            $alerts.select('pre').html(lines);
             alerts.count++;
         }
 
@@ -691,6 +706,53 @@
                 libApi[libName] = api;
             },
 
+            // TODO: implement floating panel as a class
+            // TODO: parameterize position (currently hard-coded to TopRight)
+            /*
+             * Creates div in floating panels block, with the given id.
+             * Returns panel token used to interact with the panel
+             */
+            addFloatingPanel: function (id, position) {
+                var pos = position || 'TR',
+                    el,
+                    fp;
+
+                if (fpanels[id]) {
+                    buildError('Float panel with id "' + id + '" already exists.');
+                    return null;
+                }
+
+                el = $floatPanels.append('div')
+                    .attr('id', id)
+                    .attr('class', 'fpanel');
+
+                fp = {
+                    id: id,
+                    el: el,
+                    pos: pos,
+                    show: function () {
+                        console.log('show pane: ' + id);
+                        el.transition().duration(750)
+                            .style('right', '20px')
+                            .style('opacity', 1);
+                    },
+                    hide: function () {
+                        console.log('hide pane: ' + id);
+                        el.transition().duration(750)
+                            .style('right', '-320px')
+                            .style('opacity', 0);
+                    },
+                    empty: function () {
+                        return el.html('');
+                    },
+                    append: function (what) {
+                        return el.append(what);
+                    }
+                };
+                fpanels[id] = fp;
+                return fp;
+            },
+
             // TODO: it remains to be seen whether we keep this style of docs
             /** @api ui addView( vid, nid, cb )
              * Adds a view to the UI.
@@ -782,7 +844,6 @@
             }
             built = true;
 
-            $view = d3.select('#view');
             $mastRadio = d3.select('#mastRadio');
 
             $(window).on('hashchange', hash);
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 6c0c313..aeaad2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -96,3 +96,45 @@
     fill: white;
     stroke: red;
 }
+
+
+/* detail topo-detail pane */
+
+#topo-detail {
+/* gets base CSS from .fpanel in floatPanel.css */
+}
+
+
+#topo-detail h2 {
+    margin: 8px 4px;
+    color: black;
+    vertical-align: middle;
+}
+
+#topo-detail h2 img {
+    height: 32px;
+    padding-right: 8px;
+    vertical-align: middle;
+}
+
+#topo-detail p, table {
+    margin: 4px 4px;
+}
+
+#topo-detail td.label {
+    font-style: italic;
+    color: #777;
+    padding-right: 12px;
+}
+
+#topo-detail td.value {
+
+}
+
+#topo-detail hr {
+    height: 1px;
+    color: #ccc;
+    background-color: #ccc;
+    border: 0;
+}
+
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 94b2e9e..a23f48d 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -30,6 +30,7 @@
     // configuration data
     var config = {
         useLiveData: true,
+        fnTrace: true,
         debugOn: false,
         debug: {
             showNodeXY: true,
@@ -151,7 +152,7 @@
         webSock,
         deviceLabelIndex = 0,
         hostLabelIndex = 0,
-
+        detailPane,
         selectOrder = [],
         selections = {},
 
@@ -180,12 +181,21 @@
         return config.debugOn && config.debug[what];
     }
 
+    function fnTrace(msg, id) {
+        if (config.fnTrace) {
+            console.log('FN: ' + msg + ' [' + id + ']');
+        }
+    }
 
     // ==============================
     // Key Callbacks
 
     function testMe(view) {
         view.alert('test');
+        detailPane.show();
+        setTimeout(function () {
+            detailPane.hide();
+        }, 3000);
     }
 
     function abortIfLive() {
@@ -220,7 +230,7 @@
         var v = scenario.view,
             frame;
         if (stack.length === 0) {
-            v.alert('Error:\n\nNo event #' + evn + ' found.');
+            v.alert('Oops!\n\nNo event #' + evn + ' found.');
             return;
         }
         frame = stack.shift();
@@ -279,14 +289,6 @@
         view.alert('unpin() callback')
     }
 
-    function requestPath(view) {
-        var payload = {
-            one: selections[selectOrder[0]].obj.id,
-            two: selections[selectOrder[1]].obj.id
-        }
-        sendMessage('requestPath', payload);
-    }
-
     // ==============================
     // Radio Button Callbacks
 
@@ -334,6 +336,7 @@
     function logicError(msg) {
         // TODO, report logic error to server, via websock, so it can be logged
         network.view.alert('Logic Error:\n\n' + msg);
+        console.warn(msg);
     }
 
     var eventDispatch = {
@@ -345,11 +348,13 @@
         updateHost: updateHost,
         removeDevice: stillToImplement,
         removeLink: removeLink,
-        removeHost: stillToImplement,
+        removeHost: removeHost,
+        showDetails: showDetails,
         showPath: showPath
     };
 
     function addDevice(data) {
+        fnTrace('addDevice', data.payload.id);
         var device = data.payload,
             nodeData = createDeviceNode(device);
         network.nodes.push(nodeData);
@@ -359,6 +364,7 @@
     }
 
     function addLink(data) {
+        fnTrace('addLink', data.payload.id);
         var link = data.payload,
             lnk = createLink(link);
         if (lnk) {
@@ -370,6 +376,7 @@
     }
 
     function addHost(data) {
+        fnTrace('addHost', data.payload.id);
         var host = data.payload,
             node = createHostNode(host),
             lnk;
@@ -379,6 +386,7 @@
 
         lnk = createHostLink(host);
         if (lnk) {
+            node.linkData = lnk;    // cache ref on its host
             network.links.push(lnk);
             network.lookup[host.ingress] = lnk;
             network.lookup[host.egress] = lnk;
@@ -387,7 +395,9 @@
         network.force.start();
     }
 
+    // TODO: fold updateX(...) methods into one base method; remove duplication
     function updateDevice(data) {
+        fnTrace('updateDevice', data.payload.id);
         var device = data.payload,
             id = device.id,
             nodeData = network.lookup[id];
@@ -400,6 +410,7 @@
     }
 
     function updateLink(data) {
+        fnTrace('updateLink', data.payload.id);
         var link = data.payload,
             id = link.id,
             linkData = network.lookup[id];
@@ -412,6 +423,7 @@
     }
 
     function updateHost(data) {
+        fnTrace('updateHost', data.payload.id);
         var host = data.payload,
             id = host.id,
             hostData = network.lookup[id];
@@ -423,7 +435,9 @@
         }
     }
 
+    // TODO: fold removeX(...) methods into base method - remove dup code
     function removeLink(data) {
+        fnTrace('removeLink', data.payload.id);
         var link = data.payload,
             id = link.id,
             linkData = network.lookup[id];
@@ -434,7 +448,26 @@
         }
     }
 
+    function removeHost(data) {
+        fnTrace('removeHost', data.payload.id);
+        var host = data.payload,
+            id = host.id,
+            hostData = network.lookup[id];
+        if (hostData) {
+            removeHostElement(hostData);
+        } else {
+            logicError('removeHost lookup fail. ID = "' + id + '"');
+        }
+    }
+
+    function showDetails(data) {
+        fnTrace('showDetails', data.payload.id);
+        populateDetails(data.payload);
+        detailPane.show();
+    }
+
     function showPath(data) {
+        fnTrace('showPath', data.payload.id);
         var links = data.payload.links,
             s = [ data.event + "\n" + links.length ];
         links.forEach(function (d, i) {
@@ -470,6 +503,32 @@
     }
 
     // ==============================
+    // Out-going messages...
+
+    function getSel(idx) {
+        return selections[selectOrder[idx]];
+    }
+
+    // for now, just a host-to-host intent, (and implicit start-monitoring)
+    function requestPath() {
+        var payload = {
+                one: getSel(0).obj.id,
+                two: getSel(1).obj.id
+            };
+        sendMessage('requestPath', payload);
+    }
+
+    // request details for the selected element
+    function requestDetails() {
+        var data = getSel(0).obj,
+            payload = {
+                id: data.id,
+                class: data.class
+            };
+        sendMessage('requestDetails', payload);
+    }
+
+    // ==============================
     // force layout modification functions
 
     function translate(x, y) {
@@ -589,19 +648,18 @@
         //link .foo() .bar() ...
 
         // operate on exiting links:
-        // TODO: better transition (longer as a dashed, grey line)
         link.exit()
             .attr({
                 'stroke-dasharray': '3, 3'
             })
             .style('opacity', 0.4)
             .transition()
-            .duration(2000)
+            .duration(1500)
             .attr({
                 'stroke-dasharray': '3, 12'
             })
             .transition()
-            .duration(1000)
+            .duration(500)
             .style('opacity', 0.0)
             .remove();
     }
@@ -855,17 +913,37 @@
         //node .foo() .bar() ...
 
         // operate on exiting nodes:
-        // TODO: figure out how to remove the node 'g' AND its children
-        node.exit()
+        // Note that the node is removed after 2 seconds.
+        // Sub element animations should be shorter than 2 seconds.
+        var exiting = node.exit()
             .transition()
-            .duration(750)
-            .attr({
-                opacity: 0,
-                cx: 0,
-                cy: 0,
-                r: 0
-            })
+            .duration(2000)
+            .style('opacity', 0)
             .remove();
+
+        // host node exits....
+        exiting.filter('.host').each(function (d) {
+            var node = d3.select(this);
+
+            node.select('text')
+                .style('opacity', 0.5)
+                .transition()
+                .duration(1000)
+                .style('opacity', 0);
+            // note, leave <g>.remove to remove this element
+
+            node.select('circle')
+                .style('stroke-fill', '#555')
+                .style('fill', '#888')
+                .style('opacity', 0.5)
+                .transition()
+                .duration(1500)
+                .attr('r', 0);
+            // note, leave <g>.remove to remove this element
+
+        });
+
+        // TODO: device node exits
     }
 
     function find(id, array) {
@@ -882,12 +960,27 @@
         delete network.lookup[linkData.id];
         // remove from links array
         var idx = find(linkData.id, network.links);
-
-        network.links.splice(linkData.index, 1);
+        network.links.splice(idx, 1);
         // remove from SVG
         updateLinks();
+        network.force.resume();
     }
 
+    function removeHostElement(hostData) {
+        // first, remove associated hostLink...
+        removeLinkElement(hostData.linkData);
+
+        // remove from lookup cache
+        delete network.lookup[hostData.id];
+        // remove from nodes array
+        var idx = find(hostData.id, network.nodes);
+        network.nodes.splice(idx, 1);
+        // remove from SVG
+        updateNodes();
+        network.force.resume();
+    }
+
+
     function tick() {
         node.attr({
             transform: function (d) { return translate(d.x, d.y); }
@@ -951,6 +1044,8 @@
 
     var sid = 0;
 
+    // TODO: use cache of pending messages (key = sid) to reconcile responses
+
     function sendMessage(evType, payload) {
         var toSend = {
                 event: evType,
@@ -969,7 +1064,6 @@
         wsTrace('rx', msg);
     }
     function wsTrace(rxtx, msg) {
-
         console.log('[' + rxtx + '] ' + msg);
         // TODO: integrate with trace view
         //if (trace) {
@@ -998,7 +1092,7 @@
 
         if (meta && n.classed('selected')) {
             deselectObject(obj.id);
-            //flyinPane(null);
+            updateDetailPane();
             return;
         }
 
@@ -1010,17 +1104,16 @@
         selectOrder.push(obj.id);
 
         n.classed('selected', true);
-        //flyinPane(obj);
+        updateDetailPane();
     }
 
     function deselectObject(id) {
         var obj = selections[id];
         if (obj) {
             d3.select(obj.el).classed('selected', false);
-            selections[id] = null;
-            // TODO: use splice to remove element
+            delete selections[id];
         }
-        //flyinPane(null);
+        updateDetailPane();
     }
 
     function deselectAll() {
@@ -1028,10 +1121,10 @@
         node.classed('selected', false);
         selections = {};
         selectOrder = [];
-        //flyinPane(null);
+        updateDetailPane();
     }
 
-    // TODO: this click handler does not get unloaded when the view does
+    // FIXME: this click handler does not get unloaded when the view does
     $('#view').on('click', function(e) {
         if (!$(e.target).closest('.node').length) {
             if (!e.metaKey) {
@@ -1040,6 +1133,66 @@
         }
     });
 
+    // update the state of the detail pane, based on current selections
+    function updateDetailPane() {
+        var nSel = selectOrder.length;
+        if (!nSel) {
+            detailPane.hide();
+        } else if (nSel === 1) {
+            singleSelect();
+        } else {
+            multiSelect();
+        }
+    }
+
+    function singleSelect() {
+        requestDetails();
+        // NOTE: detail pane will be shown from showDetails event.
+    }
+
+    function multiSelect() {
+        // TODO: use detail pane for multi-select view.
+        //detailPane.show();
+    }
+
+    function populateDetails(data) {
+        detailPane.empty();
+
+        var title = detailPane.append("h2"),
+            table = detailPane.append("table"),
+            tbody = table.append("tbody");
+
+        $('<img src="img/' + data.type + '.png">').appendTo(title);
+        $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
+
+        data.propOrder.forEach(function(p) {
+            if (p === '-') {
+                addSep(tbody);
+            } else {
+                addProp(tbody, p, data.props[p]);
+            }
+        });
+
+        function addSep(tbody) {
+            var tr = tbody.append('tr');
+            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
+        }
+
+        function addProp(tbody, label, value) {
+            var tr = tbody.append('tr');
+
+            tr.append('td')
+                .attr('class', 'label')
+                .text(label + ' :');
+
+            tr.append('td')
+                .attr('class', 'value')
+                .text(value);
+        }
+    }
+
+    // ==============================
+    // Test harness code
 
     function prepareScenario(view, ctx, dbg) {
         var sc = scenario,
@@ -1058,13 +1211,12 @@
         d3.json(urlSc, function(err, data) {
             var p = data && data.params || {},
                 desc = data && data.description || null,
-                intro;
+                intro = data && data.title;
 
             if (err) {
                 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
             } else {
                 sc.params = p;
-                intro = "Scenario loaded: " + ctx + '\n\n' + data.title;
                 if (desc) {
                     intro += '\n\n  ' + desc.join('\n  ');
                 }
@@ -1140,16 +1292,18 @@
             d.fixed = true;
             d3.select(self).classed('fixed', true);
             if (config.useLiveData) {
-                tellServerCoords(d);
+                sendUpdateMeta(d);
             }
         }
 
-        function tellServerCoords(d) {
+        function sendUpdateMeta(d) {
             sendMessage('updateMeta', {
                 id: d.id,
                 'class': d.class,
-                x: Math.floor(d.x),
-                y: Math.floor(d.y)
+                'memento': {
+                    x: Math.floor(d.x),
+                    y: Math.floor(d.y)
+                }
             });
         }
 
@@ -1207,4 +1361,6 @@
         resize: resize
     });
 
+    detailPane = onos.ui.addFloatingPanel('topo-detail');
+
 }(ONOS));